From 8652ee09f6a894cd7f22ea2d157868f0c2ef4981 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 2 Apr 2022 17:30:55 +0300 Subject: [PATCH] rename the sso to auth package --- HISTORY.md | 2 +- _examples/README.md | 4 +- _examples/auth/{sso => auth}/README.md | 2 +- .../sso.yml => auth/auth/auth.yml} | 9 +- _examples/auth/{sso => auth}/main.go | 42 ++++---- _examples/auth/{sso => auth}/user.go | 0 _examples/auth/{sso => auth}/user_provider.go | 6 +- .../{sso => auth}/views/layouts/main.html | 0 .../{sso => auth}/views/partials/footer.html | 0 .../auth/{sso => auth}/views/signin.html | 0 _examples/mvc/login/services/user_service.go | 2 +- .../sso.yml => mvc/websocket-auth/auth.yml} | 9 +- .../browser/index.html | 0 .../{websocket-sso => websocket-auth}/main.go | 6 +- .../{websocket-sso => websocket-auth}/user.go | 0 .../user_provider.go | 6 +- .../views/layouts/main.html | 0 .../views/partials/footer.html | 0 .../views/signin.html | 0 sso/sso.go => auth/auth.go | 102 +++++++----------- {sso => auth}/configuration.go | 67 +++++++++--- auth/provider.go | 86 +++++++++++++++ {sso => auth}/user.go | 12 +-- sso/provider.go | 83 -------------- 24 files changed, 233 insertions(+), 205 deletions(-) rename _examples/auth/{sso => auth}/README.md (74%) rename _examples/{mvc/websocket-sso/sso.yml => auth/auth/auth.yml} (85%) rename _examples/auth/{sso => auth}/main.go (75%) rename _examples/auth/{sso => auth}/user.go (100%) rename _examples/auth/{sso => auth}/user_provider.go (92%) rename _examples/auth/{sso => auth}/views/layouts/main.html (100%) rename _examples/auth/{sso => auth}/views/partials/footer.html (100%) rename _examples/auth/{sso => auth}/views/signin.html (100%) rename _examples/{auth/sso/sso.yml => mvc/websocket-auth/auth.yml} (85%) rename _examples/mvc/{websocket-sso => websocket-auth}/browser/index.html (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/main.go (93%) rename _examples/mvc/{websocket-sso => websocket-auth}/user.go (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/user_provider.go (92%) rename _examples/mvc/{websocket-sso => websocket-auth}/views/layouts/main.html (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/views/partials/footer.html (100%) rename _examples/mvc/{websocket-sso => websocket-auth}/views/signin.html (100%) rename sso/sso.go => auth/auth.go (77%) rename {sso => auth}/configuration.go (53%) create mode 100644 auth/provider.go rename {sso => auth}/user.go (69%) delete mode 100644 sso/provider.go diff --git a/HISTORY.md b/HISTORY.md index 63e3d7e6..4b08299a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -29,7 +29,7 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements - Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls. -- Add new `sso` sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). +- Add new [auth](auth) sub-package which helps on any user type auth using JWT (access & refresh tokens) and a cookie (optional). - Add `Party.EnsureStaticBindings` which, if called, the MVC binder panics if a struct's input binding depends on the HTTP request data instead of a static dependency. This is useful to make sure your API crafted through `Party.PartyConfigure` depends only on struct values you already defined at `Party.RegisterDependency` == will never use reflection at serve-time (maximum performance). diff --git a/_examples/README.md b/_examples/README.md index 78905cd2..8a7477e1 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -214,6 +214,7 @@ * [Ttemplates and Functions](i18n/template) * [Pluralization and Variables](i18n/plurals) * Authentication, Authorization & Bot Detection + * [Recommended: Auth package and Single-Sign-On](auth/auth) **NEW (GO 1.18 Generics required)** * Basic Authentication * [Basic](auth/basicauth/basic) * [Load from a slice of Users](auth/basicauth/users_list) @@ -226,7 +227,6 @@ * [Blocklist](auth/jwt/blocklist/main.go) * [Refresh Token](auth/jwt/refresh-token/main.go) * [Tutorial](auth/jwt/tutorial) - * [SSO](auth/sso) **NEW (GO 1.18 Generics required)** * [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go) * [OAUth2](auth/goth/main.go) * [Manage Permissions](auth/permissions/main.go) @@ -279,7 +279,7 @@ * [Authenticated Controller](mvc/authenticated-controller/main.go) * [Versioned Controller](mvc/versioned-controller/main.go) * [Websocket Controller](mvc/websocket) - * [Websocket + Authentication (SSO)](mvc/websocket-sso) **NEW (GO 1.18 Generics required)** + * [Websocket + Authentication (Single-Sign-On)](mvc/websocket-auth) **NEW (GO 1.18 Generics required)** * [Register Middleware](mvc/middleware) * [gRPC](mvc/grpc-compatible) * [gRPC Bidirectional Stream](mvc/grpc-compatible-bidirectional-stream) diff --git a/_examples/auth/sso/README.md b/_examples/auth/auth/README.md similarity index 74% rename from _examples/auth/sso/README.md rename to _examples/auth/auth/README.md index ffa7a823..3f91efeb 100644 --- a/_examples/auth/sso/README.md +++ b/_examples/auth/auth/README.md @@ -1,4 +1,4 @@ -# SSO (Single Sign On) +# Auth Package (+ Single Sign On) ```sh $ go run . diff --git a/_examples/mvc/websocket-sso/sso.yml b/_examples/auth/auth/auth.yml similarity index 85% rename from _examples/mvc/websocket-sso/sso.yml rename to _examples/auth/auth/auth.yml index c25a22bc..e324132f 100644 --- a/_examples/mvc/websocket-sso/sso.yml +++ b/_examples/auth/auth/auth.yml @@ -1,9 +1,12 @@ +Headers: # required. + - "Authorization" + - "X-Authorization" Cookie: # optional. - Name: "iris_sso" + Name: "iris_auth_cookie" Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). Keys: - - ID: IRIS_SSO_ACCESS # required. + - ID: IRIS_AUTH_ACCESS # required. Alg: EdDSA MaxAge: 2h # 2 hours lifetime for access tokens. Private: |+ @@ -14,7 +17,7 @@ Keys: -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4= -----END PUBLIC KEY----- - - ID: IRIS_SSO_REFRESH # optional. Good practise to have it though. + - ID: IRIS_AUTH_REFRESH # optional. Good practise to have it though. Alg: EdDSA # 1 month lifetime for refresh tokens, # after that period the user has to signin again. diff --git a/_examples/auth/sso/main.go b/_examples/auth/auth/main.go similarity index 75% rename from _examples/auth/sso/main.go rename to _examples/auth/auth/main.go index f09236a2..110948e7 100644 --- a/_examples/auth/sso/main.go +++ b/_examples/auth/auth/main.go @@ -6,10 +6,10 @@ import ( "fmt" "github.com/kataras/iris/v12" - "github.com/kataras/iris/v12/sso" + "github.com/kataras/iris/v12/auth" ) -func allowRole(role AccessRole) sso.TVerify[User] { +func allowRole(role AccessRole) auth.TVerify[User] { return func(u User) error { if !u.Role.Allow(role) { return fmt.Errorf("invalid role") @@ -19,7 +19,7 @@ func allowRole(role AccessRole) sso.TVerify[User] { } } -const configFilename = "./sso.yml" +const configFilename = "./auth.yml" func main() { app := iris.New() @@ -28,23 +28,23 @@ func main() { Layout("main")) /* - // Easiest 1-liner way, load from configuration and initialize a new sso instance: - s := sso.MustLoad[User]("./sso.yml") + // Easiest 1-liner way, load from configuration and initialize a new auth instance: + s := auth.MustLoad[User]("./auth.yml") // Bind a configuration from file: - var c sso.Configuration - c.BindFile("./sso.yml") - s, err := sso.New[User](c) + var c auth.Configuration + c.BindFile("./auth.yml") + s, err := auth.New[User](c) // OR create new programmatically configuration: - config := sso.Configuration{ + config := auth.Configuration{ ...fields } - s, err := sso.New[User](config) + s, err := auth.New[User](config) // OR generate a new configuration: - config := sso.MustGenerateConfiguration() - s, err := sso.New[User](config) + config := auth.MustGenerateConfiguration() + s, err := auth.New[User](config) // OR generate a new config and save it if cannot open the config file. if _, err := os.Stat(configFilename); err != nil { - generatedConfig := sso.MustGenerateConfiguration() + generatedConfig := auth.MustGenerateConfiguration() configContents, err := generatedConfig.ToYAML() if err != nil { panic(err) @@ -58,13 +58,13 @@ func main() { */ // 1. Load configuration from a file. - ssoConfig, err := sso.LoadConfiguration(configFilename) + authConfig, err := auth.LoadConfiguration(configFilename) if err != nil { panic(err) } - // 2. Initialize a new sso instance for "User" claims (generics: go1.18 +). - s, err := sso.New[User](ssoConfig) + // 2. Initialize a new auth instance for "User" claims (generics: go1.18 +). + s, err := auth.New[User](authConfig) if err != nil { panic(err) } @@ -72,7 +72,7 @@ func main() { // 3. Add a custom provider, in our case is just a memory-based one. s.AddProvider(NewProvider()) // 3.1. Optionally set a custom error handler. - // s.SetErrorHandler(new(sso.DefaultErrorHandler)) + // s.SetErrorHandler(new(auth.DefaultErrorHandler)) app.Get("/signin", renderSigninForm) // 4. generate token pairs. @@ -102,12 +102,12 @@ func main() { Region: "us", Tunnels: []tunnel.Tunnel{ { - Name: "Iris SSO (Test)", + Name: "Iris Auth (Test)", Addr: ":8080", Hostname: "YOUR_DOMAIN", }, { - Name: "Iris SSO (Test Subdomain)", + Name: "Iris Auth (Test Subdomain)", Addr: ":8080", Hostname: "owner.YOUR_DOMAIN", }, @@ -120,14 +120,14 @@ func renderSigninForm(ctx iris.Context) { ctx.View("signin", iris.Map{"Title": "Signin Page"}) } -func renderMemberPage(s *sso.SSO[User]) iris.Handler { +func renderMemberPage(s *auth.Auth[User]) iris.Handler { return func(ctx iris.Context) { user := s.GetUser(ctx) ctx.Writef("Hello member: %s\n", user.Email) } } -func renderOwnerPage(s *sso.SSO[User]) iris.Handler { +func renderOwnerPage(s *auth.Auth[User]) iris.Handler { return func(ctx iris.Context) { user := s.GetUser(ctx) ctx.Writef("Hello owner: %s\n", user.Email) diff --git a/_examples/auth/sso/user.go b/_examples/auth/auth/user.go similarity index 100% rename from _examples/auth/sso/user.go rename to _examples/auth/auth/user.go diff --git a/_examples/auth/sso/user_provider.go b/_examples/auth/auth/user_provider.go similarity index 92% rename from _examples/auth/sso/user_provider.go rename to _examples/auth/auth/user_provider.go index 151e426b..aedbc10a 100644 --- a/_examples/auth/sso/user_provider.go +++ b/_examples/auth/auth/user_provider.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/kataras/iris/v12/sso" + "github.com/kataras/iris/v12/auth" ) type Provider struct { @@ -49,7 +49,7 @@ func (p *Provider) Signin(ctx context.Context, username, password string) (User, return User{}, fmt.Errorf("user not found") } -func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on VerifyHandler. +func (p *Provider) ValidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on VerifyHandler. // your database and checks of blocked tokens... // check for specific token ids. @@ -81,7 +81,7 @@ func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.Standar return nil // else valid. } -func (p *Provider) InvalidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on SignoutHandler. +func (p *Provider) InvalidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on SignoutHandler. // invalidate this specific token. p.mu.Lock() p.invalidated[standardClaims.ID] = struct{}{} diff --git a/_examples/auth/sso/views/layouts/main.html b/_examples/auth/auth/views/layouts/main.html similarity index 100% rename from _examples/auth/sso/views/layouts/main.html rename to _examples/auth/auth/views/layouts/main.html diff --git a/_examples/auth/sso/views/partials/footer.html b/_examples/auth/auth/views/partials/footer.html similarity index 100% rename from _examples/auth/sso/views/partials/footer.html rename to _examples/auth/auth/views/partials/footer.html diff --git a/_examples/auth/sso/views/signin.html b/_examples/auth/auth/views/signin.html similarity index 100% rename from _examples/auth/sso/views/signin.html rename to _examples/auth/auth/views/signin.html diff --git a/_examples/mvc/login/services/user_service.go b/_examples/mvc/login/services/user_service.go index 4921c4f9..32ebf5c7 100644 --- a/_examples/mvc/login/services/user_service.go +++ b/_examples/mvc/login/services/user_service.go @@ -51,7 +51,7 @@ func (s *userService) GetByID(id int64) (datamodels.User, bool) { }) } -// GetByUsernameAndPassword returns a user based on its username and passowrd, +// GetByUsernameAndPassword returns a user based on its username and password, // used for authentication. func (s *userService) GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) { if username == "" || userPassword == "" { diff --git a/_examples/auth/sso/sso.yml b/_examples/mvc/websocket-auth/auth.yml similarity index 85% rename from _examples/auth/sso/sso.yml rename to _examples/mvc/websocket-auth/auth.yml index c25a22bc..e324132f 100644 --- a/_examples/auth/sso/sso.yml +++ b/_examples/mvc/websocket-auth/auth.yml @@ -1,9 +1,12 @@ +Headers: # required. + - "Authorization" + - "X-Authorization" Cookie: # optional. - Name: "iris_sso" + Name: "iris_auth_cookie" Hash: "D*G-KaPdSgUkXp2s5v8y/B?E(H+MbQeThWmYq3t6w9z$C&F)J@NcRfUjXn2r4u7x" # length of 64 characters (512-bit). Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit). Keys: - - ID: IRIS_SSO_ACCESS # required. + - ID: IRIS_AUTH_ACCESS # required. Alg: EdDSA MaxAge: 2h # 2 hours lifetime for access tokens. Private: |+ @@ -14,7 +17,7 @@ Keys: -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4= -----END PUBLIC KEY----- - - ID: IRIS_SSO_REFRESH # optional. Good practise to have it though. + - ID: IRIS_AUTH_REFRESH # optional. Good practise to have it though. Alg: EdDSA # 1 month lifetime for refresh tokens, # after that period the user has to signin again. diff --git a/_examples/mvc/websocket-sso/browser/index.html b/_examples/mvc/websocket-auth/browser/index.html similarity index 100% rename from _examples/mvc/websocket-sso/browser/index.html rename to _examples/mvc/websocket-auth/browser/index.html diff --git a/_examples/mvc/websocket-sso/main.go b/_examples/mvc/websocket-auth/main.go similarity index 93% rename from _examples/mvc/websocket-sso/main.go rename to _examples/mvc/websocket-auth/main.go index 1e59e264..89a52d93 100644 --- a/_examples/mvc/websocket-sso/main.go +++ b/_examples/mvc/websocket-auth/main.go @@ -6,8 +6,8 @@ import ( "fmt" "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/auth" "github.com/kataras/iris/v12/mvc" - "github.com/kataras/iris/v12/sso" "github.com/kataras/iris/v12/websocket" ) @@ -29,7 +29,7 @@ func newApp() *iris.Application { LayoutDir("layouts"). Layout("main")) - s := sso.MustLoad[User]("./sso.yml") + s := auth.MustLoad[User]("./auth.yml") s.AddProvider(NewProvider()) app.Get("/signin", renderSigninForm) @@ -63,7 +63,7 @@ func (c *websocketController) Namespace() string { func (c *websocketController) OnChat(msg websocket.Message) error { ctx := websocket.GetContext(c.Conn) - user := sso.GetUser[User](ctx) + user := auth.GetUser[User](ctx) msg.Body = []byte(fmt.Sprintf("%s: %s", user.Email, string(msg.Body))) c.Conn.Server().Broadcast(c, msg) diff --git a/_examples/mvc/websocket-sso/user.go b/_examples/mvc/websocket-auth/user.go similarity index 100% rename from _examples/mvc/websocket-sso/user.go rename to _examples/mvc/websocket-auth/user.go diff --git a/_examples/mvc/websocket-sso/user_provider.go b/_examples/mvc/websocket-auth/user_provider.go similarity index 92% rename from _examples/mvc/websocket-sso/user_provider.go rename to _examples/mvc/websocket-auth/user_provider.go index a1846c15..16fbd7ce 100644 --- a/_examples/mvc/websocket-sso/user_provider.go +++ b/_examples/mvc/websocket-auth/user_provider.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/kataras/iris/v12/sso" + "github.com/kataras/iris/v12/auth" ) type Provider struct { @@ -49,7 +49,7 @@ func (p *Provider) Signin(ctx context.Context, username, password string) (User, return User{}, fmt.Errorf("user not found") } -func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on VerifyHandler. +func (p *Provider) ValidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on VerifyHandler. // your database and checks of blocked tokens... // check for specific token ids. @@ -81,7 +81,7 @@ func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.Standar return nil // else valid. } -func (p *Provider) InvalidateToken(ctx context.Context, standardClaims sso.StandardClaims, u User) error { // fired on SignoutHandler. +func (p *Provider) InvalidateToken(ctx context.Context, standardClaims auth.StandardClaims, u User) error { // fired on SignoutHandler. // invalidate this specific token. p.mu.Lock() p.invalidated[standardClaims.ID] = struct{}{} diff --git a/_examples/mvc/websocket-sso/views/layouts/main.html b/_examples/mvc/websocket-auth/views/layouts/main.html similarity index 100% rename from _examples/mvc/websocket-sso/views/layouts/main.html rename to _examples/mvc/websocket-auth/views/layouts/main.html diff --git a/_examples/mvc/websocket-sso/views/partials/footer.html b/_examples/mvc/websocket-auth/views/partials/footer.html similarity index 100% rename from _examples/mvc/websocket-sso/views/partials/footer.html rename to _examples/mvc/websocket-auth/views/partials/footer.html diff --git a/_examples/mvc/websocket-sso/views/signin.html b/_examples/mvc/websocket-auth/views/signin.html similarity index 100% rename from _examples/mvc/websocket-sso/views/signin.html rename to _examples/mvc/websocket-auth/views/signin.html diff --git a/sso/sso.go b/auth/auth.go similarity index 77% rename from sso/sso.go rename to auth/auth.go index 047b4756..3e8d405d 100644 --- a/sso/sso.go +++ b/auth/auth.go @@ -1,6 +1,6 @@ //go:build go1.18 -package sso +package auth import ( stdContext "context" @@ -18,7 +18,7 @@ import ( ) type ( - SSO[T User] struct { + Auth[T User] struct { config Configuration keys jwt.Keys @@ -49,7 +49,7 @@ type ( } ) -func MustLoad[T User](filename string) *SSO[T] { +func MustLoad[T User](filename string) *Auth[T] { var config Configuration if err := config.BindFile(filename); err != nil { panic(err) @@ -63,7 +63,7 @@ func MustLoad[T User](filename string) *SSO[T] { return s } -func Must[T User](s *SSO[T], err error) *SSO[T] { +func Must[T User](s *Auth[T], err error) *Auth[T] { if err != nil { panic(err) } @@ -71,14 +71,14 @@ func Must[T User](s *SSO[T], err error) *SSO[T] { return s } -func New[T User](config Configuration) (*SSO[T], error) { +func New[T User](config Configuration) (*Auth[T], error) { keys, err := config.validate() if err != nil { return nil, err } _, refreshEnabled := keys[KIDRefresh] - s := &SSO[T]{ + s := &Auth[T]{ config: config, keys: keys, securecookie: securecookie.New([]byte(config.Cookie.Hash), []byte(config.Cookie.Block)), @@ -90,7 +90,7 @@ func New[T User](config Configuration) (*SSO[T], error) { return s, nil } -func (s *SSO[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *SSO[T] { +func (s *Auth[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *Auth[T] { if provider != nil { for i := range s.providers { s.providers[i] = nil @@ -108,11 +108,7 @@ func (s *SSO[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler Er return s } -func (s *SSO[T]) AddProvider(providers ...Provider[T]) *SSO[T] { - // defaultProviderTypename := strings.Replace(fmt.Sprintf("%T", s), "SSO", "provider", 1) - // if len(s.providers) == 1 && fmt.Sprintf("%T", s.providers[0]) == defaultProviderTypename { - // s.providers = append(s.providers[1:], p...) - +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. for _, p := range providers { @@ -137,22 +133,22 @@ func (s *SSO[T]) AddProvider(providers ...Provider[T]) *SSO[T] { return s } -func (s *SSO[T]) SetErrorHandler(errHandler ErrorHandler) *SSO[T] { +func (s *Auth[T]) SetErrorHandler(errHandler ErrorHandler) *Auth[T] { s.errorHandler = errHandler return s } -func (s *SSO[T]) SetTransformer(transformer Transformer[T]) *SSO[T] { +func (s *Auth[T]) SetTransformer(transformer Transformer[T]) *Auth[T] { s.transformer = transformer return s } -func (s *SSO[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *SSO[T] { +func (s *Auth[T]) SetTransformerFunc(transfermerFunc func(ctx stdContext.Context, tok *VerifiedToken) (T, error)) *Auth[T] { s.transformer = TransformerFunc[T](transfermerFunc) return s } -func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { +func (s *Auth[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) { var t T // get "t" from a valid provider. @@ -163,7 +159,7 @@ func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]by v, err := p.Signin(ctx, username, password) if err != nil { if i == n-1 { // last provider errored. - return nil, nil, fmt.Errorf("sso: signin: %w", err) + return nil, nil, fmt.Errorf("auth: signin: %w", err) } // keep searching. continue @@ -174,19 +170,19 @@ func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]by break } } else { - return nil, nil, fmt.Errorf("sso: signin: no provider") + return nil, nil, fmt.Errorf("auth: signin: no provider") } // sign the tokens. accessToken, refreshToken, err := s.sign(t) if err != nil { - return nil, nil, fmt.Errorf("sso: signin: %w", err) + return nil, nil, fmt.Errorf("auth: signin: %w", err) } return accessToken, refreshToken, nil } -func (s *SSO[T]) sign(t T) ([]byte, []byte, error) { +func (s *Auth[T]) sign(t T) ([]byte, []byte, error) { // sign the tokens. var ( accessStdClaims StandardClaims @@ -239,7 +235,7 @@ func (s *SSO[T]) sign(t T) ([]byte, []byte, error) { return accessToken, refreshToken, nil } -func (s *SSO[T]) SigninHandler(ctx *context.Context) { +func (s *Auth[T]) SigninHandler(ctx *context.Context) { // No, let the developer decide it based on a middleware, e.g. iris.LimitRequestBodySize. // ctx.SetMaxRequestBodySize(s.maxRequestBodySize) @@ -283,16 +279,16 @@ func (s *SSO[T]) SigninHandler(ctx *context.Context) { ctx.JSON(resp) } -func (s *SSO[T]) Verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { +func (s *Auth[T]) Verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { t, claims, err := s.verify(ctx, token) if err != nil { - return t, StandardClaims{}, fmt.Errorf("sso: verify: %w", err) + return t, StandardClaims{}, fmt.Errorf("auth: verify: %w", err) } return t, claims, nil } -func (s *SSO[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { +func (s *Auth[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims, error) { var t T if len(token) == 0 { // should never happen at this state. @@ -339,23 +335,7 @@ func (s *SSO[T]) verify(ctx stdContext.Context, token []byte) (T, StandardClaims return t, standardClaims, nil } -/* Good idea but not practical. -func Transform[T User, V User](transformer Transformer[T, V]) context.Handler { - return func(ctx *context.Context) { - existingUserValue := GetUser[T](ctx) - newUserValue, err := transformer.Transform(ctx, existingUserValue) - if err != nil { - ctx.SetErr(err) - return - } - - ctx.Values().Set(userContextKey, newUserValue) - ctx.Next() - } -} -*/ - -func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { +func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { return func(ctx *context.Context) { accessToken := s.extractAccessToken(ctx) @@ -376,7 +356,7 @@ func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { } if err = verify(t); err != nil { - err = fmt.Errorf("sso: verify: %v", err) + err = fmt.Errorf("auth: verify: %v", err) s.errorHandler.Unauthenticated(ctx, err) return } @@ -394,9 +374,9 @@ func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler { } } -func (s *SSO[T]) extractAccessToken(ctx *context.Context) string { +func (s *Auth[T]) extractAccessToken(ctx *context.Context) string { // first try from authorization: bearer header. - accessToken := extractTokenFromHeader(ctx) + accessToken := s.extractTokenFromHeader(ctx) // then if no header, try try extract from cookie. if accessToken == "" { @@ -408,27 +388,27 @@ func (s *SSO[T]) extractAccessToken(ctx *context.Context) string { return accessToken } -func (s *SSO[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) { +func (s *Auth[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) { if !s.refreshEnabled { - return nil, nil, fmt.Errorf("sso: refresh: disabled") + return nil, nil, fmt.Errorf("auth: refresh: disabled") } t, _, err := s.verify(ctx, refreshToken) if err != nil { - return nil, nil, fmt.Errorf("sso: refresh: %w", err) + return nil, nil, fmt.Errorf("auth: refresh: %w", err) } // refresh the tokens, both refresh & access tokens will be renew to prevent // malicious 😈 users that may hold a refresh token. accessTok, refreshTok, err := s.sign(t) if err != nil { - return nil, nil, fmt.Errorf("sso: refresh: %w", err) + return nil, nil, fmt.Errorf("auth: refresh: %w", err) } return accessTok, refreshTok, nil } -func (s *SSO[T]) RefreshHandler(ctx *context.Context) { +func (s *Auth[T]) RefreshHandler(ctx *context.Context) { var req RefreshRequest err := ctx.ReadJSON(&req) if err != nil { @@ -455,10 +435,10 @@ func (s *SSO[T]) RefreshHandler(ctx *context.Context) { ctx.JSON(resp) } -func (s *SSO[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { +func (s *Auth[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { t, standardClaims, err := s.verify(ctx, token) if err != nil { - return fmt.Errorf("sso: signout: verify: %w", err) + return fmt.Errorf("auth: signout: verify: %w", err) } for i, n := 0, len(s.providers)-1; i <= n; i++ { @@ -485,15 +465,15 @@ func (s *SSO[T]) Signout(ctx stdContext.Context, token []byte, all bool) error { return nil } -func (s *SSO[T]) SignoutHandler(ctx *context.Context) { +func (s *Auth[T]) SignoutHandler(ctx *context.Context) { s.signoutHandler(ctx, false) } -func (s *SSO[T]) SignoutAllHandler(ctx *context.Context) { +func (s *Auth[T]) SignoutAllHandler(ctx *context.Context) { s.signoutHandler(ctx, true) } -func (s *SSO[T]) signoutHandler(ctx *context.Context, all bool) { +func (s *Auth[T]) signoutHandler(ctx *context.Context, all bool) { accessToken := s.extractAccessToken(ctx) if accessToken == "" { s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing) @@ -515,13 +495,8 @@ func (s *SSO[T]) signoutHandler(ctx *context.Context, all bool) { ctx.Values().Remove(standardClaimsContextKey) } -var headerKeys = [...]string{ - "Authorization", - "X-Authorization", -} - -func extractTokenFromHeader(ctx *context.Context) string { - for _, headerKey := range headerKeys { +func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string { + for _, headerKey := range s.config.Headers { headerValue := ctx.GetHeader(headerKey) if headerValue == "" { continue @@ -539,7 +514,7 @@ func extractTokenFromHeader(ctx *context.Context) string { return "" } -func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) { +func (s *Auth[T]) trySetCookie(ctx *context.Context, accessToken string) { if cookieName := s.config.Cookie.Name; cookieName != "" { maxAge := s.keys[KIDAccess].MaxAge if maxAge == 0 { @@ -551,6 +526,7 @@ func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) { Name: cookieName, Value: url.QueryEscape(accessToken), HttpOnly: true, + Secure: ctx.IsSSL(), Domain: ctx.Domain(), SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(maxAge), @@ -561,7 +537,7 @@ func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) { } } -func (s *SSO[T]) tryRemoveCookie(ctx *context.Context) { +func (s *Auth[T]) tryRemoveCookie(ctx *context.Context) { if cookieName := s.config.Cookie.Name; cookieName != "" { ctx.RemoveCookie(cookieName) } diff --git a/sso/configuration.go b/auth/configuration.go similarity index 53% rename from sso/configuration.go rename to auth/configuration.go index e3100788..c5200541 100644 --- a/sso/configuration.go +++ b/auth/configuration.go @@ -1,6 +1,6 @@ //go:build go1.18 -package sso +package auth import ( "encoding/json" @@ -16,50 +16,77 @@ import ( ) const ( - KIDAccess = "IRIS_SSO_ACCESS" - KIDRefresh = "IRIS_SSO_REFRESH" + // The JWT Key ID for access tokens. + KIDAccess = "IRIS_AUTH_ACCESS" + // The JWT Key ID for refresh tokens. + KIDRefresh = "IRIS_AUTH_REFRESH" ) type ( + // Configuration holds the necessary information for Iris Auth & Single-Sign-On feature. + // + // See the `New` package-level function. Configuration struct { + // The authorization header keys that server should read the access token from. + // + // Defaults to: + // - Authorization + // - X-Authorization + Headers []string `json:"headers" yaml:"Headers" toml:"Headers" ini:"Headers"` + // Cookie optional configuration. + // A Cookie.Name holds the access token value fully encrypted. Cookie CookieConfiguration `json:"cookie" yaml:"Cookie" toml:"Cookie" ini:"cookie"` - // keep it to always renew the refresh token. RefreshStrategy string `json:"refresh_strategy" yaml:"RefreshStrategy" toml:"RefreshStrategy" ini:"refresh_strategy"` + // Keys MUST define the jwt keys configuration for access, + // and optionally, for refresh tokens signing and verification. Keys jwt.KeysConfiguration `json:"keys" yaml:"Keys" toml:"Keys" ini:"keys"` } + // CookieConfiguration holds the necessary information for cookie client storage. CookieConfiguration struct { - Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"name"` - Hash string `json:"hash" yaml:"Hash" toml:"Hash" ini:"hash"` + // Name defines the cookie's name. + Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"name"` + // Hash is optional, it is used to authenticate cookie value using HMAC. + // It is recommended to use a key with 32 or 64 bytes. + Hash string `json:"hash" yaml:"Hash" toml:"Hash" ini:"hash"` + // Block is optional, used to encrypt cookie value. + // The key length must correspond to the block size + // of the encryption algorithm. For AES, used by default, valid lengths are + // 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. Block string `json:"block" yaml:"Block" toml:"Block" ini:"block"` } ) func (c *Configuration) validate() (jwt.Keys, error) { + if len(c.Headers) == 0 { + return nil, fmt.Errorf("auth: configuration: headers slice is empty") + } + if c.Cookie.Name != "" { if c.Cookie.Hash == "" || c.Cookie.Block == "" { - return nil, fmt.Errorf("cookie block and cookie hash are required for security reasons when cookie is used") + return nil, fmt.Errorf("auth: configuration: cookie block and cookie hash are required for security reasons when cookie is used") } } keys, err := c.Keys.Load() if err != nil { - return nil, fmt.Errorf("sso: %w", err) + return nil, fmt.Errorf("auth: configuration: %w", err) } if _, ok := keys[KIDAccess]; !ok { - return nil, fmt.Errorf("sso: %s access token is missing from the configuration", KIDAccess) + return nil, fmt.Errorf("auth: configuration: %s access token is missing from the configuration", KIDAccess) } // Let's keep refresh optional. // if _, ok := keys[KIDRefresh]; !ok { - // return nil, fmt.Errorf("sso: %s refresh token is missing from the configuration", KIDRefresh) + // return nil, fmt.Errorf("auth: configuration: %s refresh token is missing from the configuration", KIDRefresh) // } return keys, nil } // BindRandom binds the "c" configuration to random values for keys and cookie security. // Keys will not be persisted between restarts, -// a more persistent storage should be considered for production applications. +// a more persistent storage should be considered for production applications, +// see BindFile method and LoadConfiguration/MustLoadConfiguration package-level functions. func (c *Configuration) BindRandom() error { accessPublic, accessPrivate, err := jwt.GenerateEdDSA() if err != nil { @@ -72,8 +99,12 @@ func (c *Configuration) BindRandom() error { } *c = Configuration{ + Headers: []string{ + "Authorization", + "X-Authorization", + }, Cookie: CookieConfiguration{ - Name: "iris_sso", + Name: "iris_auth_cookie", Hash: string(securecookie.GenerateRandomKey(64)), Block: string(securecookie.GenerateRandomKey(32)), }, @@ -99,6 +130,9 @@ func (c *Configuration) BindRandom() error { return nil } +// BindFile binds a filename (fullpath) to "c" Configuration. +// The file format is either JSON or YAML and it should be suffixed +// with .json or .yml/.yaml. func (c *Configuration) BindFile(filename string) error { switch filepath.Ext(filename) { case ".json": @@ -131,14 +165,18 @@ func (c *Configuration) BindFile(filename string) error { } +// ToYAML returns the "c" Configuration's contents as raw yaml byte slice. func (c *Configuration) ToYAML() ([]byte, error) { return yaml.Marshal(c) } +// ToJSON returns the "c" Configuration's contents as raw json byte slice. func (c *Configuration) ToJSON() ([]byte, error) { return json.Marshal(c) } +// MustGenerateConfiguration calls the Configuration's BindRandom +// method and returns the result. It panics on errors. func MustGenerateConfiguration() (c Configuration) { if err := c.BindRandom(); err != nil { panic(err) @@ -147,11 +185,16 @@ 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. func MustLoadConfiguration(filename string) Configuration { c, err := LoadConfiguration(filename) if err != nil { diff --git a/auth/provider.go b/auth/provider.go new file mode 100644 index 00000000..1c1ccf9a --- /dev/null +++ b/auth/provider.go @@ -0,0 +1,86 @@ +//go:build go1.18 + +package auth + +import ( + stdContext "context" + + "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/x/errors" + + "github.com/kataras/jwt" +) + +// VerifiedToken holds the information about a verified token. +type VerifiedToken = jwt.VerifiedToken + +// Provider is an interface of T which MUST be completed +// by a custom value type to provide user information to the Auth's +// JWT Token Signer and Verifier. +// +// A provider can implement Transformer and ErrorHandler and ClaimsProvider as well. +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 + Signin(ctx stdContext.Context, username, password string) (T, error) + + // ValidateToken accepts the standard JWT claims and the T value obtained + // by the Signin method and should return a nil error on validation success + // or a non-nil error for validation failure. + // It is mostly used to perform checks of the T value's struct fields or + // the standard claim's (e.g. origin jwt token id). + // It can be an empty method too which returns a nil error. + // + // It's caleld on auth.VerifyHandler. + ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error + + // InvalidateToken is optional and can be used to allow tokens to be invalidated + // from server-side. Commonly, implement when a token and user pair is saved + // 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. + // + // 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. + // + // It's called on auth.SignoutAllHandler. + InvalidateTokens(ctx stdContext.Context, t T) error +} + +// ClaimsProvider is an optional interface, which may not be used at all. +// If implemented by a Provider, it signs the jwt token +// using these claims to each of the following token types. +type ClaimsProvider interface { + GetAccessTokenClaims() StandardClaims + GetRefreshTokenClaims(accessClaims StandardClaims) StandardClaims +} + +type Transformer[T User] interface { + Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) +} + +type TransformerFunc[T User] func(ctx stdContext.Context, tok *VerifiedToken) (T, error) + +func (fn TransformerFunc[T]) Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) { + return fn(ctx, tok) +} + +type ErrorHandler interface { + InvalidArgument(ctx *context.Context, err error) + Unauthenticated(ctx *context.Context, err error) +} + +type DefaultErrorHandler struct{} + +func (e *DefaultErrorHandler) InvalidArgument(ctx *context.Context, err error) { + errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) +} + +func (e *DefaultErrorHandler) Unauthenticated(ctx *context.Context, err error) { + errors.Unauthenticated.Err(ctx, err) +} diff --git a/sso/user.go b/auth/user.go similarity index 69% rename from sso/user.go rename to auth/user.go index 5cd0b010..f08b18ab 100644 --- a/sso/user.go +++ b/auth/user.go @@ -1,6 +1,6 @@ //go:build go1.18 -package sso +package auth import ( "github.com/kataras/iris/v12/context" @@ -13,13 +13,13 @@ type ( User = interface{} // any type. ) -const accessTokenContextKey = "iris.sso.context.access_token" +const accessTokenContextKey = "iris.auth.context.access_token" func GetAccessToken(ctx *context.Context) string { return ctx.Values().GetString(accessTokenContextKey) } -const standardClaimsContextKey = "iris.sso.context.standard_claims" +const standardClaimsContextKey = "iris.auth.context.standard_claims" func GetStandardClaims(ctx *context.Context) StandardClaims { if v := ctx.Values().Get(standardClaimsContextKey); v != nil { @@ -31,11 +31,11 @@ func GetStandardClaims(ctx *context.Context) StandardClaims { return StandardClaims{} } -func (s *SSO[T]) GetStandardClaims(ctx *context.Context) StandardClaims { +func (s *Auth[T]) GetStandardClaims(ctx *context.Context) StandardClaims { return GetStandardClaims(ctx) } -const userContextKey = "iris.sso.context.user" +const userContextKey = "iris.auth.context.user" func GetUser[T User](ctx *context.Context) T { if v := ctx.Values().Get(userContextKey); v != nil { @@ -48,6 +48,6 @@ func GetUser[T User](ctx *context.Context) T { return empty } -func (s *SSO[T]) GetUser(ctx *context.Context) T { +func (s *Auth[T]) GetUser(ctx *context.Context) T { return GetUser[T](ctx) } diff --git a/sso/provider.go b/sso/provider.go deleted file mode 100644 index 816c2d79..00000000 --- a/sso/provider.go +++ /dev/null @@ -1,83 +0,0 @@ -//go:build go1.18 - -package sso - -import ( - stdContext "context" - "fmt" - - "github.com/kataras/iris/v12/context" - "github.com/kataras/iris/v12/middleware/jwt" - "github.com/kataras/iris/v12/x/errors" -) - -type VerifiedToken = jwt.VerifiedToken - -type Provider[T User] interface { // A provider can implement Transformer and ErrorHandler as well. - Signin(ctx stdContext.Context, username, password string) (T, error) - - // We could do this instead of transformer below but let's keep separated logic methods: - // ValidateToken(ctx context.Context, tok *VerifiedToken, t *T) error - ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error - - InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error - InvalidateTokens(ctx stdContext.Context, t T) error -} - -// ClaimsProvider is an optional interface, which may not be used at all. -// If completed by a Provider, it signs the jwt token -// using these claims to each of the following token types. -type ClaimsProvider interface { - GetAccessTokenClaims() StandardClaims - GetRefreshTokenClaims(accessClaims StandardClaims) StandardClaims -} - -type Transformer[T User] interface { - Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) -} - -type TransformerFunc[T User] func(ctx stdContext.Context, tok *VerifiedToken) (T, error) - -func (fn TransformerFunc[T]) Transform(ctx stdContext.Context, tok *VerifiedToken) (T, error) { - return fn(ctx, tok) -} - -type ErrorHandler interface { - InvalidArgument(ctx *context.Context, err error) - Unauthenticated(ctx *context.Context, err error) -} - -type DefaultErrorHandler struct{} - -func (e *DefaultErrorHandler) InvalidArgument(ctx *context.Context, err error) { - errors.InvalidArgument.Details(ctx, "unable to parse body", err.Error()) -} - -func (e *DefaultErrorHandler) Unauthenticated(ctx *context.Context, err error) { - errors.Unauthenticated.Err(ctx, err) -} - -type provider[T User] struct{} - -func newProvider[T User]() *provider[T] { - return new(provider[T]) -} - -func (p *provider[T]) Signin(ctx stdContext.Context, username, password string) (T, error) { // fired on SigninHandler. - // your database... - var t T - return t, fmt.Errorf("user not found") -} - -func (p *provider[T]) ValidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error { // fired on VerifyHandler. - // your database and checks of blocked tokens... - return nil -} - -func (p *provider[T]) InvalidateToken(ctx stdContext.Context, standardClaims StandardClaims, t T) error { // fired on SignoutHandler. - return nil -} - -func (p *provider[T]) InvalidateTokens(ctx stdContext.Context, t T) error { // fired on SignoutAllHandler. - return nil -}