Add Context.SetUser and Context.User methods

relative to: https://github.com/iris-contrib/middleware/issues/63
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-10-12 15:52:53 +03:00
parent dfe27567ae
commit 8e51a296b9
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
7 changed files with 242 additions and 19 deletions

View File

@ -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).

View File

@ -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) {

View File

@ -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.

144
context/context_user.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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")

View File

@ -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