From 8e51a296b96893ab5538ab36f335b7af94cf014b Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 12 Oct 2020 15:52:53 +0300 Subject: [PATCH] Add Context.SetUser and Context.User methods relative to: https://github.com/iris-contrib/middleware/issues/63 --- HISTORY.md | 2 + _examples/auth/basicauth/main.go | 7 +- context/context.go | 26 ++++- context/context_user.go | 144 +++++++++++++++++++++++++ middleware/basicauth/basicauth.go | 61 ++++++++--- middleware/basicauth/basicauth_test.go | 16 +++ middleware/basicauth/config.go | 5 +- 7 files changed, 242 insertions(+), 19 deletions(-) create mode 100644 context/context_user.go diff --git a/HISTORY.md b/HISTORY.md index ce404469..95ad69f3 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,7 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below. - A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/main.go) client credentials. - Add the ability to [share functions](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services). @@ -309,6 +310,7 @@ var dirOpts = iris.DirOptions{ ## New Context Methods +- `Context.SetUser(User)` and `Context.User() User` to store and retrieve an authenticated client. Read more [here](https://github.com/iris-contrib/middleware/issues/63). - `Context.SetLogoutFunc(fn interface{}, persistenceArgs ...interface{})` and `Logout(args ...interface{}) error` methods to allow different kind of auth middlewares to be able to set a "logout" a user/client feature with a single function, the route handler may not be aware of the implementation of the authentication used. - `Context.SetFunc(name string, fn interface{}, persistenceArgs ...interface{})` and `Context.CallFunc(name string, args ...interface{}) ([]reflect.Value, error)` to allow middlewares to share functions dynamically when the type of the function is not predictable, see the [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) for more. - `Context.TextYAML(interface{}) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text). diff --git a/_examples/auth/basicauth/main.go b/_examples/auth/basicauth/main.go index 2486efb2..80ecae42 100644 --- a/_examples/auth/basicauth/main.go +++ b/_examples/auth/basicauth/main.go @@ -51,11 +51,12 @@ func main() { } func h(ctx iris.Context) { - username, password, _ := ctx.Request().BasicAuth() + // username, password, _ := ctx.Request().BasicAuth() // third parameter it will be always true because the middleware // makes sure for that, otherwise this handler will not be executed. - - ctx.Writef("%s %s:%s", ctx.Path(), username, password) + // OR: + user := ctx.User() + ctx.Writef("%s %s:%s", ctx.Path(), user.GetUsername(), user.GetPassword()) } func logout(ctx iris.Context) { diff --git a/context/context.go b/context/context.go index efe354ac..42b62393 100644 --- a/context/context.go +++ b/context/context.go @@ -2133,12 +2133,12 @@ const disableRequestBodyConsumptionContextKey = "iris.request.body.record" // but acts for the current request. // It makes the request body readable more than once. func (ctx *Context) RecordBody() { - ctx.Values().Set(disableRequestBodyConsumptionContextKey, true) + ctx.values.Set(disableRequestBodyConsumptionContextKey, true) } // IsRecordingBody reports whether the request body can be readen multiple times. func (ctx *Context) IsRecordingBody() bool { - return ctx.Values().GetBoolDefault(disableRequestBodyConsumptionContextKey, + return ctx.values.GetBoolDefault(disableRequestBodyConsumptionContextKey, ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal()) } @@ -5253,6 +5253,28 @@ func (ctx *Context) Logout(args ...interface{}) error { return err } +const userContextKey = "iris.user" + +// SetUser sets a User for this request. +// It's used by auth middlewares as a common +// method to provide user information to the +// next handlers in the chain. +func (ctx *Context) SetUser(u User) { + ctx.values.Set(userContextKey, u) +} + +// User returns the registered User of this request. +// See `SetUser` too. +func (ctx *Context) User() User { + if v := ctx.values.Get(userContextKey); v != nil { + if u, ok := v.(User); ok { + return u + } + } + + return nil +} + const idContextKey = "iris.context.id" // SetID sets an ID, any value, to the Request Context. diff --git a/context/context_user.go b/context/context_user.go new file mode 100644 index 00000000..093f3dcd --- /dev/null +++ b/context/context_user.go @@ -0,0 +1,144 @@ +package context + +import ( + "errors" + "time" +) + +// ErrNotSupported is fired when a specific method is not implemented +// or not supported entirely. +// Can be used by User implementations when +// an authentication system does not implement a specific, but required, +// method of the User interface. +var ErrNotSupported = errors.New("not supported") + +// User is a generic view of an authorized client. +// See `Context.User` and `SetUser` methods for more. +// +// The informational methods starts with a "Get" prefix +// in order to allow the implementation to contain exported +// fields such as `Username` so they can be JSON encoded when necessary. +// +// The caller is free to cast this with the implementation directly +// when special features are offered by the authorization system. +type User interface { + // GetAuthorization should return the authorization method, + // e.g. Basic Authentication. + GetAuthorization() string + // GetAuthorizedAt should return the exact time the + // client has been authorized for the "first" time. + GetAuthorizedAt() time.Time + // GetUsername should return the name of the User. + GetUsername() string + // GetPassword should return the encoded or raw password + // (depends on the implementation) of the User. + GetPassword() string + // GetEmail should return the e-mail of the User. + GetEmail() string +} + +// FeaturedUser optional interface that a User can implement. +type FeaturedUser interface { + User + // GetFeatures should optionally return a list of features + // the User implementation offers. + GetFeatures() []UserFeature +} + +// UserFeature a type which represents a user's optional feature. +// See `HasUserFeature` function for more. +type UserFeature uint32 + +// The list of standard UserFeatures. +const ( + AuthorizedAtFeature UserFeature = iota + UsernameFeature + PasswordFeature + EmailFeature +) + +// HasUserFeature reports whether the "u" User +// implements a specific "feature" User Feature. +// +// It returns ErrNotSupported if a user does not implement +// the FeaturedUser interface. +func HasUserFeature(user User, feature UserFeature) (bool, error) { + if u, ok := user.(FeaturedUser); ok { + for _, f := range u.GetFeatures() { + if f == feature { + return true, nil + } + } + + return false, nil + } + + return false, ErrNotSupported +} + +// SimpleUser is a simple implementation of the User interface. +type SimpleUser struct { + Authorization string `json:"authorization"` + AuthorizedAt time.Time `json:"authorized_at"` + Username string `json:"username"` + Password string `json:"-"` + Email string `json:"email,omitempty"` + Features []UserFeature `json:"-"` +} + +var _ User = (*SimpleUser)(nil) + +// GetAuthorization returns the authorization method, +// e.g. Basic Authentication. +func (u *SimpleUser) GetAuthorization() string { + return u.Authorization +} + +// GetAuthorizedAt returns the exact time the +// client has been authorized for the "first" time. +func (u *SimpleUser) GetAuthorizedAt() time.Time { + return u.AuthorizedAt +} + +// GetUsername returns the name of the User. +func (u *SimpleUser) GetUsername() string { + return u.Username +} + +// GetPassword returns the raw password of the User. +func (u *SimpleUser) GetPassword() string { + return u.Password +} + +// GetEmail returns the e-mail of the User. +func (u *SimpleUser) GetEmail() string { + return u.Email +} + +// GetFeatures returns a list of features +// this User implementation offers. +func (u *SimpleUser) GetFeatures() []UserFeature { + if u.Features != nil { + return u.Features + } + + var features []UserFeature + + if !u.AuthorizedAt.IsZero() { + features = append(features, AuthorizedAtFeature) + } + + if u.Username != "" { + features = append(features, UsernameFeature) + } + + if u.Password != "" { + features = append(features, PasswordFeature) + } + + if u.Email != "" { + features = append(features, EmailFeature) + } + + return features +} diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index 5b71ae8f..a1c83b5c 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -1,7 +1,11 @@ // Package basicauth provides http basic authentication via middleware. See _examples/auth/basicauth package basicauth -// test file: ../../_examples/auth/basicauth/main_test.go +/* +Test files: + - ../../_examples/auth/basicauth/main_test.go + - ./basicauth_test.go +*/ import ( "encoding/base64" @@ -16,14 +20,18 @@ func init() { context.SetHandlerName("iris/middleware/basicauth.*", "iris.basicauth") } +const authorizationType = "Basic Authentication" + type ( encodedUser struct { - HeaderValue string - Username string - logged bool - forceLogout bool // in order to be able to invalidate and use a redirect response. - expires time.Time - mu sync.RWMutex + HeaderValue string + Username string + Password string + logged bool + forceLogout bool // in order to be able to invalidate and use a redirect response. + authorizedAt time.Time // when from !logged to logged. + expires time.Time + mu sync.RWMutex } basicAuthMiddleware struct { @@ -45,6 +53,8 @@ type ( // which will ask the client for basic auth (username, password), // validate that and if valid continues to the next handler, otherwise // throws a StatusUnauthorized http error code. +// +// Use the `Context.User` method to retrieve the stored user. func New(c Config) context.Handler { config := DefaultConfig() if c.Realm != "" { @@ -76,7 +86,13 @@ func (b *basicAuthMiddleware) init() { for k, v := range b.config.Users { fullUser := k + ":" + v header := "Basic " + base64.StdEncoding.EncodeToString([]byte(fullUser)) - b.auth = append(b.auth, &encodedUser{HeaderValue: header, Username: k, logged: false, expires: DefaultExpireTime}) + b.auth = append(b.auth, &encodedUser{ + HeaderValue: header, + Username: k, + Password: v, + logged: false, + expires: DefaultExpireTime, + }) } // set the auth realm header's value @@ -106,7 +122,8 @@ func (b *basicAuthMiddleware) askForCredentials(ctx *context.Context) { } } -// Serve the actual middleware +// Serve the actual basic authentication middleware. +// Use the Context.User method to retrieve the stored user. func (b *basicAuthMiddleware) Serve(ctx *context.Context) { auth, found := b.findAuth(ctx.GetHeader("Authorization")) if !found || auth.forceLogout { @@ -122,11 +139,20 @@ func (b *basicAuthMiddleware) Serve(ctx *context.Context) { // don't continue to the next handler } + auth.mu.RLock() + logged := auth.logged + auth.mu.RUnlock() + if !logged { + auth.mu.Lock() + auth.authorizedAt = time.Now() + auth.mu.Unlock() + } + // all ok if b.expireEnabled { - if !auth.logged { + if !logged { auth.mu.Lock() - auth.expires = time.Now().Add(b.config.Expires) + auth.expires = auth.authorizedAt.Add(b.config.Expires) auth.logged = true auth.mu.Unlock() } @@ -137,6 +163,7 @@ func (b *basicAuthMiddleware) Serve(ctx *context.Context) { if expired { auth.mu.Lock() auth.logged = false + auth.forceLogout = false auth.mu.Unlock() b.askForCredentials(ctx) // ask for authentication again ctx.StopExecution() @@ -144,8 +171,18 @@ func (b *basicAuthMiddleware) Serve(ctx *context.Context) { } } - if !b.config.DisableLogoutFunc { + if !b.config.DisableContextUser { ctx.SetLogoutFunc(b.Logout) + + auth.mu.RLock() + user := &context.SimpleUser{ + Authorization: authorizationType, + AuthorizedAt: auth.authorizedAt, + Username: auth.Username, + Password: auth.Password, + } + auth.mu.RUnlock() + ctx.SetUser(user) } ctx.Next() // continue diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index 76406a51..5ff44ab1 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -20,6 +20,15 @@ func TestBasicAuthUseRouter(t *testing.T) { app.UseRouter(basicauth.Default(users)) + app.Get("/user_json", func(ctx iris.Context) { + ctx.JSON(ctx.User()) + }) + + app.Get("/user_string", func(ctx iris.Context) { + user := ctx.User() + ctx.Writef("%s\n%s\n%s", user.GetAuthorization(), user.GetUsername(), user.GetPassword()) + }) + app.Get("/", func(ctx iris.Context) { username, _, _ := ctx.Request().BasicAuth() ctx.Writef("Hello, %s!", username) @@ -55,6 +64,13 @@ func TestBasicAuthUseRouter(t *testing.T) { // Test pass authentication and route found. e.GET("/").WithBasicAuth(username, password).Expect(). Status(httptest.StatusOK).Body().Equal(fmt.Sprintf("Hello, %s!", username)) + e.GET("/user_json").WithBasicAuth(username, password).Expect(). + Status(httptest.StatusOK).JSON().Object().ContainsMap(iris.Map{ + "username": username, + }) + e.GET("/user_string").WithBasicAuth(username, password).Expect(). + Status(httptest.StatusOK).Body(). + Equal(fmt.Sprintf("%s\n%s\n%s", "Basic Authentication", username, password)) // Test empty auth. e.GET("/").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized") diff --git a/middleware/basicauth/config.go b/middleware/basicauth/config.go index ddd76316..70396215 100644 --- a/middleware/basicauth/config.go +++ b/middleware/basicauth/config.go @@ -43,8 +43,9 @@ type Config struct { // Defaults to nil. OnAsk context.Handler - // DisableLogoutFunc disables the registration of the custom basicauth Context.Logout. - DisableLogoutFunc bool + // DisableContextUser disables the registration of the custom basicauth Context.Logout + // and the User. + DisableContextUser bool } // DefaultConfig returns the default configs for the BasicAuth middleware