diff --git a/_examples/auth/auth/main.go b/_examples/auth/auth/main.go index 110948e7..c05a4133 100644 --- a/_examples/auth/auth/main.go +++ b/_examples/auth/auth/main.go @@ -9,7 +9,7 @@ import ( "github.com/kataras/iris/v12/auth" ) -func allowRole(role AccessRole) auth.TVerify[User] { +func allowRole(role AccessRole) auth.VerifyUserFunc[User] { return func(u User) error { if !u.Role.Allow(role) { return fmt.Errorf("invalid role") diff --git a/auth/auth.go b/auth/auth.go index 3d1537bf..b1c752f7 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -18,51 +18,87 @@ import ( ) type ( + // Auth holds the necessary functionality to authorize and optionally authenticating + // users to access and perform actions against the resource server (Iris API). + // It completes a secure and fast JSON Web Token signer and verifier which, + // based on the custom application needs, can be further customized. + // Each Auth of T instance can sign and verify a single custom instance, + // more Auth instances can share the same configuration to support multiple custom user types. + // Initialize a new Auth of T instance using the New or MustLoad package-level functions. + // Most important methods of the instance are: + // - AddProvider + // - SigninHandler + // - VerifyHandler + // - SignoutHandler + // - SignoutAllHandler + // + // Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/main.go. Auth[T User] struct { + // Holds the configuration passed through the New and MustLoad + // package-level functions. One or more Auth instance can share the + // same configuration's values. config Configuration - - keys jwt.Keys + // Holds the result of the config.KeysConfiguration. + keys jwt.Keys + // This is an Iris cookie option used to encrypt and decrypt a cookie when + // the config.Cookie.Hash & Block are not empty. securecookie context.SecureCookie - - providers []Provider[T] // at least one. - errorHandler ErrorHandler - transformer Transformer[T] + // Defaults to an empty list, which cannot sign any tokens. + // One or more custom providers should be registered through + // the AddProvider or WithProviderAndErrorHandler methods. + providers []Provider[T] // at least one. + // Always not nil, set to custom error handler on SetErrorHandler. + errorHandler ErrorHandler + // Not nil if a transformer is registered. + transformer Transformer[T] + // Not nil if a custom claims provider is registered. claimsProvider ClaimsProvider - refreshEnabled bool // if KIDRefresh exists in keys. + // True if KIDRefresh on config.Keys. + refreshEnabled bool } - TVerify[T User] func(t T) error + // VerifyUserFunc is passed on Verify and VerifyHandler method + // to, optionally, further validate a T user value. + VerifyUserFunc[T User] func(t T) error + // SigninRequest is the request body the server expects + // on SignHandler. The Password and Username or Email should be filled. SigninRequest struct { Username string `json:"username" form:"username,omitempty"` // username OR email, username has priority over email. Email string `json:"email" form:"email,omitempty"` // email OR username. Password string `json:"password" form:"password"` } + // SigninResponse is the response body the server sends + // to the client on the SignHandler. It contains a pair of the access token + // and the refresh token if the refresh jwt token id exists in the configuration. SigninResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` } + // RefreshRequest is the request body the server expects + // on VerifyHandler to renew an access and refresh token pair. RefreshRequest struct { RefreshToken string `json:"refresh_token"` } ) +// MustLoad binds a filename (fullpath) configuration yaml or json +// and constructs a new Auth instance. It panics on error. func MustLoad[T User](filename string) *Auth[T] { var config Configuration if err := config.BindFile(filename); err != nil { panic(err) } - s, err := New[T](config) - if err != nil { - panic(err) - } - - return s + return Must(New[T](config)) } +// Must is a helper that wraps a call to a function returning (*Auth[T], error) +// and panics if the error is non-nil. It is intended for use in variable +// initializations such as +// var s = auth.Must(auth.New[MyUser](config)) func Must[T User](s *Auth[T], err error) *Auth[T] { if err != nil { panic(err) @@ -71,6 +107,14 @@ func Must[T User](s *Auth[T], err error) *Auth[T] { return s } +// New initializes a new Auth instance typeof T and returns it. +// The T generic can be any custom struct. +// It accepts a Configuration value which can be constructed +// manually or through a configuration file using the +// MustGenerateConfiguration or MustLoadConfiguration +// or LoadConfiguration or MustLoad package-level functions. +// +// Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/main.go. func New[T User](config Configuration) (*Auth[T], error) { keys, err := config.validate() if err != nil { @@ -121,6 +165,8 @@ func (s *Auth[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler E return s } +// AddProvider registers one or more providers to this Auth of T instance and returns itself. +// Look the Provider godoc for more. func (s *Auth[T]) AddProvider(providers ...Provider[T]) *Auth[T] { // A provider can also implement both transformer and // error handler if that's the design option of the end-developer. @@ -146,21 +192,31 @@ func (s *Auth[T]) AddProvider(providers ...Provider[T]) *Auth[T] { return s } +// SetErrorHandler sets a custom error handler to this Auth of T instance and returns itself. +// Look the Provider and ErrorHandler godoc for more. func (s *Auth[T]) SetErrorHandler(errHandler ErrorHandler) *Auth[T] { s.errorHandler = errHandler return s } +// SetTransformer sets a custom transformer to this Auth of T instance and returns itself. +// Look the Provider and Transformer godoc for more. func (s *Auth[T]) SetTransformer(transformer Transformer[T]) *Auth[T] { s.transformer = transformer return s } +// SetTransformerFunc like SetTransformer method but accepts a function instead. func (s *Auth[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *Auth[T] { s.transformer = TransformerFunc[T](transfermerFunc) return s } +// Signin signs a token based on the provided username and password +// and returns a pair of access and refresh tokens. +// +// Signin calls the Provider.Signin method to check if a user +// is authenticated by the given username and password combination. func (s *Auth[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { var t T @@ -292,12 +348,22 @@ func (s *Auth[T]) SigninHandler(ctx *context.Context) { ctx.JSON(resp) } -func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { +func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte, verifyFuncs ...VerifyUserFunc[T]) (T, StandardClaims, error) { t, claims, err := s.verify(ctx, token) if err != nil { return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) } + for _, verify := range verifyFuncs { + if verify == nil { + continue + } + + if err = verify(t); err != nil { + return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) + } + } + return t, claims, nil } @@ -348,7 +414,7 @@ func (s *Auth[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaim return t, standardClaims, nil } -func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { +func (s *Auth[T]) VerifyHandler(verifyFuncs ...VerifyUserFunc[T]) context.Handler { return func(ctx *context.Context) { accessToken := s.extractAccessToken(ctx) @@ -357,31 +423,18 @@ func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { return } - t, claims, err := s.Verify(ctx, []byte(accessToken)) + t, claims, err := s.Verify(ctx, []byte(accessToken), verifyFuncs...) if err != nil { s.errorHandler.Unauthenticated(ctx, err) return } - for _, verify := range verifyFuncs { - if verify == nil { - continue - } - - if err = verify(t); err != nil { - err = fmt.Errorf("auth: verify: %v", err) - s.errorHandler.Unauthenticated(ctx, err) - return - } - } - ctx.SetUser(t) // store the user to the request. ctx.Values().Set(accessTokenContextKey, accessToken) - - ctx.Values().Set(userContextKey, t) ctx.Values().Set(standardClaimsContextKey, claims) + ctx.Values().Set(userContextKey, t) ctx.Next() } @@ -504,8 +557,8 @@ func (s *Auth[T]) signoutHandler(ctx *context.Context, all bool) { ctx.SetUser(nil) ctx.Values().Remove(accessTokenContextKey) - ctx.Values().Remove(userContextKey) ctx.Values().Remove(standardClaimsContextKey) + ctx.Values().Remove(userContextKey) } func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string { @@ -535,9 +588,9 @@ func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { } cookie := &http.Cookie{ - Path: "/", Name: cookieName, Value: url.QueryEscape(accessToken), + Path: "/", HttpOnly: true, Secure: s.config.Cookie.Secure || ctx.IsSSL(), Domain: ctx.Domain(), @@ -546,7 +599,7 @@ func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { MaxAge: int(maxAge.Seconds()), } - ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie)) + ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie), context.CookieAllowReclaim()) } } diff --git a/auth/configuration.go b/auth/configuration.go index eb81ea47..fb3ea57e 100644 --- a/auth/configuration.go +++ b/auth/configuration.go @@ -192,16 +192,8 @@ func MustGenerateConfiguration() (c Configuration) { return } -// LoadConfiguration reads a filename (fullpath) -// and returns a Configuration binded to the contents of the given filename. -// See Configuration.BindFile method too. -func LoadConfiguration(filename string) (c Configuration, err error) { - err = c.BindFile(filename) - return -} - // MustLoadConfiguration same as LoadConfiguration package-level function -// but it panics on errors. +// but it panics on error. func MustLoadConfiguration(filename string) Configuration { c, err := LoadConfiguration(filename) if err != nil { @@ -210,3 +202,11 @@ func MustLoadConfiguration(filename string) Configuration { return c } + +// LoadConfiguration reads a filename (fullpath) +// and returns a Configuration binded to the contents of the given filename. +// See Configuration.BindFile method too. +func LoadConfiguration(filename string) (c Configuration, err error) { + err = c.BindFile(filename) + return +} diff --git a/auth/provider.go b/auth/provider.go index 5e2171bb..9261e60f 100644 --- a/auth/provider.go +++ b/auth/provider.go @@ -21,12 +21,17 @@ type VerifiedToken = jwt.VerifiedToken // A provider can optionally complete the Transformer, ClaimsProvider and // ErrorHandler all in once when necessary. // Set a provider using the AddProvider method of Auth type. +// +// Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/user_provider.go. type Provider[T User] interface { // Signin accepts a username (or email) and a password and should // return a valid T value or an error describing // the user authentication or verification reason of failure. // - // It's called on Auth.SigninHandler + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.SigninHandler. + // + // It's called on Auth.SigninHandler. Signin(ctx stdContext.Context, username, password string) (T, error) // ValidateToken accepts the standard JWT claims and the T value obtained @@ -36,6 +41,9 @@ type Provider[T User] interface { // the standard claim's (e.g. origin jwt token id). // It can be an empty method too which returns a nil error. // + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.VerifyHandler. + // // It's caleld on Auth.VerifyHandler. ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error @@ -44,12 +52,18 @@ type Provider[T User] interface { // on a persistence storage and server can decide which token is valid or invalid. // It can be an empty method too which returns a nil error. // + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.SignoutHandler. + // // It's called on Auth.SignoutHandler. InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error // InvalidateTokens is like InvalidateToken but it should invalidate // all tokens generated for a specific T value. // It can be an empty method too which returns a nil error. // + // The first input argument standard context can be + // casted to iris.Context if executed through Auth.SignoutAllHandler. + // // It's called on Auth.SignoutAllHandler. InvalidateTokens(ctx stdContext.Context, t T) error } @@ -70,6 +84,9 @@ type ClaimsProvider interface { // A transformer is called on Auth.VerifyHandler before Provider.ValidateToken and it can // be used to modify the T value based on the token's contents. It is mostly used // to convert the json claims to T value manually, when they differ. +// +// The first input argument standard context can be +// casted to iris.Context if executed through Auth.VerifyHandler. type Transformer[T User] interface { Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) } diff --git a/auth/user.go b/auth/user.go index 885afd20..9958077d 100644 --- a/auth/user.go +++ b/auth/user.go @@ -13,7 +13,9 @@ type ( StandardClaims = jwt.Claims // User is an alias of an empty interface, it's here to declare the typeof T, // which can be any custom struct type. - User = interface{} + // + // Example can be found at: https://github.com/kataras/iris/tree/master/_examples/auth/auth/user.go. + User = any ) const accessTokenContextKey = "iris.auth.context.access_token" diff --git a/context/context.go b/context/context.go index 030f41b8..01e63c2c 100644 --- a/context/context.go +++ b/context/context.go @@ -3865,9 +3865,10 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, shouldOptimize boo err error ) - if !shouldOptimize && options.Indent == "" { - options.Indent = " " - } + // Let's keep it as it is. + // if !shouldOptimize && options.Indent == "" { + // options.Indent = " " + // } if indent := options.Indent; indent != "" { if shouldOptimize {