add context partial user helper and accept a generic interface on SetUser - the same method now returns an error if the given value does not complete at least one method of the User interface

This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-10-31 15:47:28 +02:00
parent 8eea0296a7
commit 3d59d19de6
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
8 changed files with 356 additions and 191 deletions

View File

@ -56,7 +56,9 @@ func h(ctx iris.Context) {
// makes sure for that, otherwise this handler will not be executed.
// OR:
user := ctx.User()
ctx.Writef("%s %s:%s", ctx.Path(), user.GetUsername(), user.GetPassword())
username, _ := user.GetUsername()
password, _ := user.GetPassword
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
}
func logout(ctx iris.Context) {

View File

@ -26,14 +26,14 @@ func main() {
signer := jwt.NewSigner(jwt.HS256, sigKey, 10*time.Minute)
// Enable payload encryption with:
// signer.WithGCM(encKey, nil)
// signer.WithEncryption(encKey, nil)
app.Get("/", generateToken(signer))
verifier := jwt.NewVerifier(jwt.HS256, sigKey)
// Enable server-side token block feature (even before its expiration time):
verifier.WithDefaultBlocklist()
// Enable payload decryption with:
// verifier.WithGCM(encKey, nil)
// verifier.WithDecryption(encKey, nil)
verifyMiddleware := verifier.Verify(func() interface{} {
return new(fooClaims)
})

View File

@ -28,6 +28,21 @@ type UserClaims struct {
Username string `json:"username"`
}
// GetID implements the partial context user's ID interface.
// Note that if claims were a map then the claims value converted to UserClaims
// and no need to implement any method.
//
// This is useful when multiple auth methods are used (e.g. basic auth, jwt)
// but they all share a couple of methods.
func (u *UserClaims) GetID() string {
return u.ID
}
// GetUsername implements the partial context user's Username interface.
func (u *UserClaims) GetUsername() string {
return u.Username
}
// Validate completes the middleware's custom ClaimsValidator.
// It will not accept a token which its claims missing the username field
// (useful to not accept refresh tokens generated by the same algorithm).
@ -58,8 +73,15 @@ func main() {
protectedAPI.Use(verifyMiddleware)
protectedAPI.Get("/", func(ctx iris.Context) {
claims := jwt.Get(ctx).(*UserClaims)
ctx.Writef("Username: %s\n", claims.Username)
// Access the claims through: jwt.Get:
// claims := jwt.Get(ctx).(*UserClaims)
// ctx.Writef("Username: %s\n", claims.Username)
//
// OR through context's user (if at least one method was implement by our UserClaims):
user := ctx.User()
id, _ := user.GetID()
username, _ := user.GetUsername()
ctx.Writef("ID: %s\nUsername: %s\n", id, username)
})
}

View File

@ -5340,12 +5340,34 @@ func (ctx *Context) Logout(args ...interface{}) error {
const userContextKey = "iris.user"
// SetUser sets a User for this request.
// SetUser sets a value as 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) {
// next handlers in the chain
// Look the `User` method to retrieve it.
func (ctx *Context) SetUser(i interface{}) error {
if i == nil {
ctx.values.Remove(userContextKey)
return nil
}
u, ok := i.(User)
if !ok {
if m, ok := i.(Map); ok { // it's a map, convert it to a User.
u = UserMap(m)
} else {
// It's a structure, wrap it and let
// runtime decide the features.
p := newUserPartial(i)
if p == nil {
return ErrNotSupported
}
u = p
}
}
ctx.values.Set(userContextKey, u)
return nil
}
// User returns the registered User of this request.

View File

@ -1,6 +1,7 @@
package context
import (
"encoding/json"
"errors"
"strings"
"time"
@ -27,30 +28,33 @@ var ErrNotSupported = errors.New("not supported")
// To make optional some of the fields you can just embed the User interface
// and implement whatever methods you want to support.
//
// There are two builtin implementations of the User interface:
// - SimpleUser (type-safe)
// - UserMap (wraps a map[string]interface{})
// There are three builtin implementations of the User interface:
// - SimpleUser
// - UserMap (a wrapper by SetUser)
// - UserPartial (a wrapper by SetUser)
type User interface {
// GetAuthorization should return the authorization method,
// e.g. Basic Authentication.
GetAuthorization() string
GetAuthorization() (string, error)
// GetAuthorizedAt should return the exact time the
// client has been authorized for the "first" time.
GetAuthorizedAt() time.Time
GetAuthorizedAt() (time.Time, error)
// GetID should return the ID of the User.
GetID() (string, error)
// GetUsername should return the name of the User.
GetUsername() string
GetUsername() (string, error)
// GetPassword should return the encoded or raw password
// (depends on the implementation) of the User.
GetPassword() string
GetPassword() (string, error)
// GetEmail should return the e-mail of the User.
GetEmail() string
GetEmail() (string, error)
// GetRoles should optionally return the specific user's roles.
// Returns `ErrNotSupported` if this method is not
// implemented by the User implementation.
GetRoles() ([]string, error)
// GetToken should optionally return a token used
// to authorize this User.
GetToken() (string, error)
GetToken() ([]byte, error)
// GetField should optionally return a dynamic field
// based on its key. Useful for custom user fields.
// Keep in mind that these fields are encoded as a separate JSON key.
@ -63,6 +67,7 @@ add a Raw() interface{} to return the underline User implementation too.
The advandages of the above idea is that we don't have to add new methods
for each of the builtin features and we can keep the (assumed) struct small.
But we dont as it has many disadvantages, unless is requested.
^ UPDATE: this is done through UserPartial.
The disadvantage of the current implementation is that the developer MUST
complete the whole interface in order to be a valid User and if we add
@ -72,88 +77,51 @@ We kind of by-pass this disadvantage by providing a SimpleUser which can be embe
to the end-developer's custom implementations.
*/
// 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
RolesFeature
TokenFeature
FieldsFeature
)
// 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,omitempty"`
Password string `json:"-"`
Email string `json:"email,omitempty"`
Roles []string `json:"roles,omitempty"`
Features []UserFeature `json:"features,omitempty"`
Token string `json:"token,omitempty"`
Fields Map `json:"fields,omitempty"`
Authorization string `json:"authorization,omitempty"`
AuthorizedAt time.Time `json:"authorized_at,omitempty"`
ID string `json:"id,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"-"`
Email string `json:"email,omitempty"`
Roles []string `json:"roles,omitempty"`
Token json.RawMessage `json:"token,omitempty"`
Fields Map `json:"fields,omitempty"`
}
var _ FeaturedUser = (*SimpleUser)(nil)
var _ User = (*SimpleUser)(nil)
// GetAuthorization returns the authorization method,
// e.g. Basic Authentication.
func (u *SimpleUser) GetAuthorization() string {
return u.Authorization
func (u *SimpleUser) GetAuthorization() (string, error) {
return u.Authorization, nil
}
// GetAuthorizedAt returns the exact time the
// client has been authorized for the "first" time.
func (u *SimpleUser) GetAuthorizedAt() time.Time {
return u.AuthorizedAt
func (u *SimpleUser) GetAuthorizedAt() (time.Time, error) {
return u.AuthorizedAt, nil
}
// GetID returns the ID of the User.
func (u *SimpleUser) GetID() (string, error) {
return u.ID, nil
}
// GetUsername returns the name of the User.
func (u *SimpleUser) GetUsername() string {
return u.Username
func (u *SimpleUser) GetUsername() (string, error) {
return u.Username, nil
}
// GetPassword returns the raw password of the User.
func (u *SimpleUser) GetPassword() string {
return u.Password
func (u *SimpleUser) GetPassword() (string, error) {
return u.Password, nil
}
// GetEmail returns the e-mail of the User.
func (u *SimpleUser) GetEmail() string {
return u.Email
// GetEmail returns the e-mail of (string,error) User.
func (u *SimpleUser) GetEmail() (string, error) {
return u.Email, nil
}
// GetRoles returns the specific user's roles.
@ -171,9 +139,9 @@ func (u *SimpleUser) GetRoles() ([]string, error) {
//
// The implementation can change that behavior.
// Returns with `ErrNotSupported` if the Token field is empty.
func (u *SimpleUser) GetToken() (string, error) {
if u.Token == "" {
return "", ErrNotSupported
func (u *SimpleUser) GetToken() ([]byte, error) {
if len(u.Token) == 0 {
return nil, ErrNotSupported
}
return u.Token, nil
@ -189,104 +157,66 @@ func (u *SimpleUser) GetField(key string) (interface{}, error) {
return u.Fields[key], nil
}
// 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)
}
if u.Roles != nil {
features = append(features, RolesFeature)
}
if u.Fields != nil {
features = append(features, FieldsFeature)
}
return features
}
// UserMap can be used to convert a common map[string]interface{} to a User.
// Usage:
// user := map[string]interface{}{
// "username": "kataras",
// "age" : 27,
// }
// ctx.SetUser(UserMap(user))
// ctx.SetUser(user)
// OR
// user := UserMap{"key": "value",...}
// user := UserStruct{....}
// ctx.SetUser(user)
// [...]
// username := ctx.User().GetUsername()
// age := ctx.User().GetField("age").(int)
// username, err := ctx.User().GetUsername()
// field,err := ctx.User().GetField("age")
// age := field.(int)
// OR cast it:
// user := ctx.User().(UserMap)
// username := user["username"].(string)
// age := user["age"].(int)
type UserMap Map
var _ FeaturedUser = UserMap{}
var _ User = UserMap{}
// GetAuthorization returns the authorization or Authorization value of the map.
func (u UserMap) GetAuthorization() string {
func (u UserMap) GetAuthorization() (string, error) {
return u.str("authorization")
}
// GetAuthorizedAt returns the authorized_at or Authorized_At value of the map.
func (u UserMap) GetAuthorizedAt() time.Time {
func (u UserMap) GetAuthorizedAt() (time.Time, error) {
return u.time("authorized_at")
}
// GetID returns the id or Id or ID value of the map.
func (u UserMap) GetID() (string, error) {
return u.str("id")
}
// GetUsername returns the username or Username value of the map.
func (u UserMap) GetUsername() string {
func (u UserMap) GetUsername() (string, error) {
return u.str("username")
}
// GetPassword returns the password or Password value of the map.
func (u UserMap) GetPassword() string {
func (u UserMap) GetPassword() (string, error) {
return u.str("password")
}
// GetEmail returns the email or Email value of the map.
func (u UserMap) GetEmail() string {
func (u UserMap) GetEmail() (string, error) {
return u.str("email")
}
// GetRoles returns the roles or Roles value of the map.
func (u UserMap) GetRoles() ([]string, error) {
if s := u.strSlice("roles"); s != nil {
return s, nil
}
return nil, ErrNotSupported
return u.strSlice("roles")
}
// GetToken returns the roles or Roles value of the map.
func (u UserMap) GetToken() (string, error) {
if s := u.str("token"); s != "" {
return s, nil
}
return "", ErrNotSupported
func (u UserMap) GetToken() ([]byte, error) {
return u.bytes("token")
}
// GetField returns the raw map's value based on its "key".
@ -295,41 +225,6 @@ func (u UserMap) GetField(key string) (interface{}, error) {
return u[key], nil
}
// GetFeatures returns a list of features
// this map offers.
func (u UserMap) GetFeatures() []UserFeature {
if v := u.val("features"); v != nil { // if already contain features.
if features, ok := v.([]UserFeature); ok {
return features
}
}
// else try to resolve from map values.
features := []UserFeature{FieldsFeature}
if !u.GetAuthorizedAt().IsZero() {
features = append(features, AuthorizedAtFeature)
}
if u.GetUsername() != "" {
features = append(features, UsernameFeature)
}
if u.GetPassword() != "" {
features = append(features, PasswordFeature)
}
if u.GetEmail() != "" {
features = append(features, EmailFeature)
}
if roles, err := u.GetRoles(); err == nil && roles != nil {
features = append(features, RolesFeature)
}
return features
}
func (u UserMap) val(key string) interface{} {
isTitle := unicode.IsTitle(rune(key[0])) // if starts with uppercase.
if isTitle {
@ -339,34 +234,248 @@ func (u UserMap) val(key string) interface{} {
return u[key]
}
func (u UserMap) str(key string) string {
func (u UserMap) bytes(key string) ([]byte, error) {
if v := u.val(key); v != nil {
switch s := v.(type) {
case []byte:
return s, nil
case string:
return []byte(s), nil
}
}
return nil, ErrNotSupported
}
func (u UserMap) str(key string) (string, error) {
if v := u.val(key); v != nil {
if s, ok := v.(string); ok {
return s
return s, nil
}
// exists or not we don't care, if it's invalid type we don't fill it.
}
return ""
return "", ErrNotSupported
}
func (u UserMap) strSlice(key string) []string {
func (u UserMap) strSlice(key string) ([]string, error) {
if v := u.val(key); v != nil {
if s, ok := v.([]string); ok {
return s
return s, nil
}
}
return nil
return nil, ErrNotSupported
}
func (u UserMap) time(key string) time.Time {
func (u UserMap) time(key string) (time.Time, error) {
if v := u.val(key); v != nil {
if t, ok := v.(time.Time); ok {
return t
return t, nil
}
}
return time.Time{}
return time.Time{}, ErrNotSupported
}
type (
userGetAuthorization interface {
GetAuthorization() string
}
userGetAuthorizedAt interface {
GetAuthorizedAt() time.Time
}
userGetID interface {
GetID() string
}
userGetUsername interface {
GetUsername() string
}
userGetPassword interface {
GetPassword() string
}
userGetEmail interface {
GetEmail() string
}
userGetRoles interface {
GetRoles() []string
}
userGetToken interface {
GetToken() []byte
}
userGetField interface {
GetField(string) interface{}
}
// UserPartial is a User.
// It's a helper which wraps a struct value that
// may or may not complete the whole User interface.
UserPartial struct {
Raw interface{}
userGetAuthorization
userGetAuthorizedAt
userGetID
userGetUsername
userGetPassword
userGetEmail
userGetRoles
userGetToken
userGetField
}
)
var _ User = (*UserPartial)(nil)
func newUserPartial(i interface{}) *UserPartial {
containsAtLeastOneMethod := false
p := &UserPartial{Raw: i}
if u, ok := i.(userGetAuthorization); ok {
p.userGetAuthorization = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetAuthorizedAt); ok {
p.userGetAuthorizedAt = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetID); ok {
p.userGetID = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetUsername); ok {
p.userGetUsername = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetPassword); ok {
p.userGetPassword = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetEmail); ok {
p.userGetEmail = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetRoles); ok {
p.userGetRoles = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetToken); ok {
p.userGetToken = u
containsAtLeastOneMethod = true
}
if u, ok := i.(userGetField); ok {
p.userGetField = u
containsAtLeastOneMethod = true
}
if !containsAtLeastOneMethod {
return nil
}
return p
}
// GetAuthorization should return the authorization method,
// e.g. Basic Authentication.
func (u *UserPartial) GetAuthorization() (string, error) {
if v := u.userGetAuthorization; v != nil {
return v.GetAuthorization(), nil
}
return "", ErrNotSupported
}
// GetAuthorizedAt should return the exact time the
// client has been authorized for the "first" time.
func (u *UserPartial) GetAuthorizedAt() (time.Time, error) {
if v := u.userGetAuthorizedAt; v != nil {
return v.GetAuthorizedAt(), nil
}
return time.Time{}, ErrNotSupported
}
// GetID should return the ID of the User.
func (u *UserPartial) GetID() (string, error) {
if v := u.userGetID; v != nil {
return v.GetID(), nil
}
return "", ErrNotSupported
}
// GetUsername should return the name of the User.
func (u *UserPartial) GetUsername() (string, error) {
if v := u.userGetUsername; v != nil {
return v.GetUsername(), nil
}
return "", ErrNotSupported
}
// GetPassword should return the encoded or raw password
// (depends on the implementation) of the User.
func (u *UserPartial) GetPassword() (string, error) {
if v := u.userGetPassword; v != nil {
return v.GetPassword(), nil
}
return "", ErrNotSupported
}
// GetEmail should return the e-mail of the User.
func (u *UserPartial) GetEmail() (string, error) {
if v := u.userGetEmail; v != nil {
return v.GetEmail(), nil
}
return "", ErrNotSupported
}
// GetRoles should optionally return the specific user's roles.
// Returns `ErrNotSupported` if this method is not
// implemented by the User implementation.
func (u *UserPartial) GetRoles() ([]string, error) {
if v := u.userGetRoles; v != nil {
return v.GetRoles(), nil
}
return nil, ErrNotSupported
}
// GetToken should optionally return a token used
// to authorize this User.
func (u *UserPartial) GetToken() ([]byte, error) {
if v := u.userGetToken; v != nil {
return v.GetToken(), nil
}
return nil, ErrNotSupported
}
// GetField should optionally return a dynamic field
// based on its key. Useful for custom user fields.
// Keep in mind that these fields are encoded as a separate JSON key.
func (u *UserPartial) GetField(key string) (interface{}, error) {
if v := u.userGetField; v != nil {
return v.GetField(key), nil
}
return nil, ErrNotSupported
}

View File

@ -26,7 +26,11 @@ func TestBasicAuthUseRouter(t *testing.T) {
app.Get("/user_string", func(ctx iris.Context) {
user := ctx.User()
ctx.Writef("%s\n%s\n%s", user.GetAuthorization(), user.GetUsername(), user.GetPassword())
authorization, _ := user.GetAuthorization()
username, _ := user.GetUsername()
password, _ := user.GetPassword()
ctx.Writef("%s\n%s\n%s", authorization, username, password)
})
app.Get("/", func(ctx iris.Context) {

View File

@ -24,8 +24,8 @@ func NewSigner(signatureAlg Alg, signatureKey interface{}, maxAge time.Duration)
}
}
// WithGCM enables AES-GCM payload decryption.
func (s *Signer) WithGCM(key, additionalData []byte) *Signer {
// WithEncryption enables AES-GCM payload decryption.
func (s *Signer) WithEncryption(key, additionalData []byte) *Signer {
encrypt, _, err := jwt.GCM(key, additionalData)
if err != nil {
panic(err) // important error before serve, stop everything.
@ -35,10 +35,14 @@ func (s *Signer) WithGCM(key, additionalData []byte) *Signer {
return s
}
// Sign generates a new token based on the given "claims" which is valid up to "s.MaxAge".
func (s *Signer) Sign(claims interface{}, opts ...SignOption) ([]byte, error) {
return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, append([]SignOption{MaxAge(s.MaxAge)}, opts...)...)
}
// NewTokenPair accepts the access and refresh claims plus the life time duration for the refresh token
// and generates a new token pair which can be sent to the client.
// The same token pair can be json-decoded.
func (s *Signer) NewTokenPair(accessClaims interface{}, refreshClaims interface{}, refreshMaxAge time.Duration, accessOpts ...SignOption) (TokenPair, error) {
if refreshMaxAge <= s.MaxAge {
return TokenPair{}, fmt.Errorf("refresh max age should be bigger than access token's one[%d - %d]", refreshMaxAge, s.MaxAge)

View File

@ -49,6 +49,8 @@ type Verifier struct {
Validators []TokenValidator
ErrorHandler func(ctx *context.Context, err error)
// DisableContextUser disables the registration of the claims as context User.
DisableContextUser bool
}
// NewVerifier accepts the algorithm for the token's signature among with its (private) key
@ -67,8 +69,8 @@ func NewVerifier(signatureAlg Alg, signatureKey interface{}, validators ...Token
}
}
// WithGCM enables AES-GCM payload encryption.
func (v *Verifier) WithGCM(key, additionalData []byte) *Verifier {
// WithDecryption enables AES-GCM payload encryption.
func (v *Verifier) WithDecryption(key, additionalData []byte) *Verifier {
_, decrypt, err := jwt.GCM(key, additionalData)
if err != nil {
panic(err) // important error before serve, stop everything.
@ -176,8 +178,8 @@ func (v *Verifier) Verify(claimsType func() interface{}, validators ...TokenVali
}
}
if u, ok := dest.(context.User); ok {
ctx.SetUser(u)
if !v.DisableContextUser {
ctx.SetUser(dest)
}
ctx.Values().Set(claimsContextKey, dest)