mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 18:51:03 +01:00
rename the sso to auth package
This commit is contained in:
parent
60e19de9e2
commit
8652ee09f6
|
@ -29,7 +29,7 @@ The codebase for Dependency Injection, Internationalization and localization and
|
||||||
## Fixes and Improvements
|
## Fixes and Improvements
|
||||||
|
|
||||||
- Add `Context.SetJSONOptions` to customize on a higher level the JSON options on `Context.JSON` calls.
|
- 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).
|
- 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).
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,7 @@
|
||||||
* [Ttemplates and Functions](i18n/template)
|
* [Ttemplates and Functions](i18n/template)
|
||||||
* [Pluralization and Variables](i18n/plurals)
|
* [Pluralization and Variables](i18n/plurals)
|
||||||
* Authentication, Authorization & Bot Detection
|
* Authentication, Authorization & Bot Detection
|
||||||
|
* [Recommended: Auth package and Single-Sign-On](auth/auth) **NEW (GO 1.18 Generics required)**
|
||||||
* Basic Authentication
|
* Basic Authentication
|
||||||
* [Basic](auth/basicauth/basic)
|
* [Basic](auth/basicauth/basic)
|
||||||
* [Load from a slice of Users](auth/basicauth/users_list)
|
* [Load from a slice of Users](auth/basicauth/users_list)
|
||||||
|
@ -226,7 +227,6 @@
|
||||||
* [Blocklist](auth/jwt/blocklist/main.go)
|
* [Blocklist](auth/jwt/blocklist/main.go)
|
||||||
* [Refresh Token](auth/jwt/refresh-token/main.go)
|
* [Refresh Token](auth/jwt/refresh-token/main.go)
|
||||||
* [Tutorial](auth/jwt/tutorial)
|
* [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)
|
* [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
|
||||||
* [OAUth2](auth/goth/main.go)
|
* [OAUth2](auth/goth/main.go)
|
||||||
* [Manage Permissions](auth/permissions/main.go)
|
* [Manage Permissions](auth/permissions/main.go)
|
||||||
|
@ -279,7 +279,7 @@
|
||||||
* [Authenticated Controller](mvc/authenticated-controller/main.go)
|
* [Authenticated Controller](mvc/authenticated-controller/main.go)
|
||||||
* [Versioned Controller](mvc/versioned-controller/main.go)
|
* [Versioned Controller](mvc/versioned-controller/main.go)
|
||||||
* [Websocket Controller](mvc/websocket)
|
* [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)
|
* [Register Middleware](mvc/middleware)
|
||||||
* [gRPC](mvc/grpc-compatible)
|
* [gRPC](mvc/grpc-compatible)
|
||||||
* [gRPC Bidirectional Stream](mvc/grpc-compatible-bidirectional-stream)
|
* [gRPC Bidirectional Stream](mvc/grpc-compatible-bidirectional-stream)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# SSO (Single Sign On)
|
# Auth Package (+ Single Sign On)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ go run .
|
$ go run .
|
|
@ -1,9 +1,12 @@
|
||||||
|
Headers: # required.
|
||||||
|
- "Authorization"
|
||||||
|
- "X-Authorization"
|
||||||
Cookie: # optional.
|
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).
|
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).
|
Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit).
|
||||||
Keys:
|
Keys:
|
||||||
- ID: IRIS_SSO_ACCESS # required.
|
- ID: IRIS_AUTH_ACCESS # required.
|
||||||
Alg: EdDSA
|
Alg: EdDSA
|
||||||
MaxAge: 2h # 2 hours lifetime for access tokens.
|
MaxAge: 2h # 2 hours lifetime for access tokens.
|
||||||
Private: |+
|
Private: |+
|
||||||
|
@ -14,7 +17,7 @@ Keys:
|
||||||
-----BEGIN PUBLIC KEY-----
|
-----BEGIN PUBLIC KEY-----
|
||||||
MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4=
|
MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4=
|
||||||
-----END PUBLIC KEY-----
|
-----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
|
Alg: EdDSA
|
||||||
# 1 month lifetime for refresh tokens,
|
# 1 month lifetime for refresh tokens,
|
||||||
# after that period the user has to signin again.
|
# after that period the user has to signin again.
|
|
@ -6,10 +6,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"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 {
|
return func(u User) error {
|
||||||
if !u.Role.Allow(role) {
|
if !u.Role.Allow(role) {
|
||||||
return fmt.Errorf("invalid 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() {
|
func main() {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
@ -28,23 +28,23 @@ func main() {
|
||||||
Layout("main"))
|
Layout("main"))
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// Easiest 1-liner way, load from configuration and initialize a new sso instance:
|
// Easiest 1-liner way, load from configuration and initialize a new auth instance:
|
||||||
s := sso.MustLoad[User]("./sso.yml")
|
s := auth.MustLoad[User]("./auth.yml")
|
||||||
// Bind a configuration from file:
|
// Bind a configuration from file:
|
||||||
var c sso.Configuration
|
var c auth.Configuration
|
||||||
c.BindFile("./sso.yml")
|
c.BindFile("./auth.yml")
|
||||||
s, err := sso.New[User](c)
|
s, err := auth.New[User](c)
|
||||||
// OR create new programmatically configuration:
|
// OR create new programmatically configuration:
|
||||||
config := sso.Configuration{
|
config := auth.Configuration{
|
||||||
...fields
|
...fields
|
||||||
}
|
}
|
||||||
s, err := sso.New[User](config)
|
s, err := auth.New[User](config)
|
||||||
// OR generate a new configuration:
|
// OR generate a new configuration:
|
||||||
config := sso.MustGenerateConfiguration()
|
config := auth.MustGenerateConfiguration()
|
||||||
s, err := sso.New[User](config)
|
s, err := auth.New[User](config)
|
||||||
// OR generate a new config and save it if cannot open the config file.
|
// OR generate a new config and save it if cannot open the config file.
|
||||||
if _, err := os.Stat(configFilename); err != nil {
|
if _, err := os.Stat(configFilename); err != nil {
|
||||||
generatedConfig := sso.MustGenerateConfiguration()
|
generatedConfig := auth.MustGenerateConfiguration()
|
||||||
configContents, err := generatedConfig.ToYAML()
|
configContents, err := generatedConfig.ToYAML()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -58,13 +58,13 @@ func main() {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 1. Load configuration from a file.
|
// 1. Load configuration from a file.
|
||||||
ssoConfig, err := sso.LoadConfiguration(configFilename)
|
authConfig, err := auth.LoadConfiguration(configFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Initialize a new sso instance for "User" claims (generics: go1.18 +).
|
// 2. Initialize a new auth instance for "User" claims (generics: go1.18 +).
|
||||||
s, err := sso.New[User](ssoConfig)
|
s, err := auth.New[User](authConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ func main() {
|
||||||
// 3. Add a custom provider, in our case is just a memory-based one.
|
// 3. Add a custom provider, in our case is just a memory-based one.
|
||||||
s.AddProvider(NewProvider())
|
s.AddProvider(NewProvider())
|
||||||
// 3.1. Optionally set a custom error handler.
|
// 3.1. Optionally set a custom error handler.
|
||||||
// s.SetErrorHandler(new(sso.DefaultErrorHandler))
|
// s.SetErrorHandler(new(auth.DefaultErrorHandler))
|
||||||
|
|
||||||
app.Get("/signin", renderSigninForm)
|
app.Get("/signin", renderSigninForm)
|
||||||
// 4. generate token pairs.
|
// 4. generate token pairs.
|
||||||
|
@ -102,12 +102,12 @@ func main() {
|
||||||
Region: "us",
|
Region: "us",
|
||||||
Tunnels: []tunnel.Tunnel{
|
Tunnels: []tunnel.Tunnel{
|
||||||
{
|
{
|
||||||
Name: "Iris SSO (Test)",
|
Name: "Iris Auth (Test)",
|
||||||
Addr: ":8080",
|
Addr: ":8080",
|
||||||
Hostname: "YOUR_DOMAIN",
|
Hostname: "YOUR_DOMAIN",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Iris SSO (Test Subdomain)",
|
Name: "Iris Auth (Test Subdomain)",
|
||||||
Addr: ":8080",
|
Addr: ":8080",
|
||||||
Hostname: "owner.YOUR_DOMAIN",
|
Hostname: "owner.YOUR_DOMAIN",
|
||||||
},
|
},
|
||||||
|
@ -120,14 +120,14 @@ func renderSigninForm(ctx iris.Context) {
|
||||||
ctx.View("signin", iris.Map{"Title": "Signin Page"})
|
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) {
|
return func(ctx iris.Context) {
|
||||||
user := s.GetUser(ctx)
|
user := s.GetUser(ctx)
|
||||||
ctx.Writef("Hello member: %s\n", user.Email)
|
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) {
|
return func(ctx iris.Context) {
|
||||||
user := s.GetUser(ctx)
|
user := s.GetUser(ctx)
|
||||||
ctx.Writef("Hello owner: %s\n", user.Email)
|
ctx.Writef("Hello owner: %s\n", user.Email)
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/sso"
|
"github.com/kataras/iris/v12/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
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")
|
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...
|
// your database and checks of blocked tokens...
|
||||||
|
|
||||||
// check for specific token ids.
|
// check for specific token ids.
|
||||||
|
@ -81,7 +81,7 @@ func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.Standar
|
||||||
return nil // else valid.
|
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.
|
// invalidate this specific token.
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.invalidated[standardClaims.ID] = struct{}{}
|
p.invalidated[standardClaims.ID] = struct{}{}
|
|
@ -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.
|
// used for authentication.
|
||||||
func (s *userService) GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) {
|
func (s *userService) GetByUsernameAndPassword(username, userPassword string) (datamodels.User, bool) {
|
||||||
if username == "" || userPassword == "" {
|
if username == "" || userPassword == "" {
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
Headers: # required.
|
||||||
|
- "Authorization"
|
||||||
|
- "X-Authorization"
|
||||||
Cookie: # optional.
|
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).
|
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).
|
Block: "VkYp3s6v9y$B&E)H@McQfTjWmZq4t7w!" # length of 32 characters (256-bit).
|
||||||
Keys:
|
Keys:
|
||||||
- ID: IRIS_SSO_ACCESS # required.
|
- ID: IRIS_AUTH_ACCESS # required.
|
||||||
Alg: EdDSA
|
Alg: EdDSA
|
||||||
MaxAge: 2h # 2 hours lifetime for access tokens.
|
MaxAge: 2h # 2 hours lifetime for access tokens.
|
||||||
Private: |+
|
Private: |+
|
||||||
|
@ -14,7 +17,7 @@ Keys:
|
||||||
-----BEGIN PUBLIC KEY-----
|
-----BEGIN PUBLIC KEY-----
|
||||||
MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4=
|
MCowBQYDK2VwAyEAzpgjKSr9E032DX+foiOxq1QDsbzjLxagTN+yVpGWZB4=
|
||||||
-----END PUBLIC KEY-----
|
-----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
|
Alg: EdDSA
|
||||||
# 1 month lifetime for refresh tokens,
|
# 1 month lifetime for refresh tokens,
|
||||||
# after that period the user has to signin again.
|
# after that period the user has to signin again.
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/auth"
|
||||||
"github.com/kataras/iris/v12/mvc"
|
"github.com/kataras/iris/v12/mvc"
|
||||||
"github.com/kataras/iris/v12/sso"
|
|
||||||
"github.com/kataras/iris/v12/websocket"
|
"github.com/kataras/iris/v12/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ func newApp() *iris.Application {
|
||||||
LayoutDir("layouts").
|
LayoutDir("layouts").
|
||||||
Layout("main"))
|
Layout("main"))
|
||||||
|
|
||||||
s := sso.MustLoad[User]("./sso.yml")
|
s := auth.MustLoad[User]("./auth.yml")
|
||||||
s.AddProvider(NewProvider())
|
s.AddProvider(NewProvider())
|
||||||
|
|
||||||
app.Get("/signin", renderSigninForm)
|
app.Get("/signin", renderSigninForm)
|
||||||
|
@ -63,7 +63,7 @@ func (c *websocketController) Namespace() string {
|
||||||
|
|
||||||
func (c *websocketController) OnChat(msg websocket.Message) error {
|
func (c *websocketController) OnChat(msg websocket.Message) error {
|
||||||
ctx := websocket.GetContext(c.Conn)
|
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)))
|
msg.Body = []byte(fmt.Sprintf("%s: %s", user.Email, string(msg.Body)))
|
||||||
c.Conn.Server().Broadcast(c, msg)
|
c.Conn.Server().Broadcast(c, msg)
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/sso"
|
"github.com/kataras/iris/v12/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Provider struct {
|
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")
|
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...
|
// your database and checks of blocked tokens...
|
||||||
|
|
||||||
// check for specific token ids.
|
// check for specific token ids.
|
||||||
|
@ -81,7 +81,7 @@ func (p *Provider) ValidateToken(ctx context.Context, standardClaims sso.Standar
|
||||||
return nil // else valid.
|
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.
|
// invalidate this specific token.
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
p.invalidated[standardClaims.ID] = struct{}{}
|
p.invalidated[standardClaims.ID] = struct{}{}
|
|
@ -1,6 +1,6 @@
|
||||||
//go:build go1.18
|
//go:build go1.18
|
||||||
|
|
||||||
package sso
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
stdContext "context"
|
stdContext "context"
|
||||||
|
@ -18,7 +18,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
SSO[T User] struct {
|
Auth[T User] struct {
|
||||||
config Configuration
|
config Configuration
|
||||||
|
|
||||||
keys jwt.Keys
|
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
|
var config Configuration
|
||||||
if err := config.BindFile(filename); err != nil {
|
if err := config.BindFile(filename); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -63,7 +63,7 @@ func MustLoad[T User](filename string) *SSO[T] {
|
||||||
return s
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -71,14 +71,14 @@ func Must[T User](s *SSO[T], err error) *SSO[T] {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func New[T User](config Configuration) (*SSO[T], error) {
|
func New[T User](config Configuration) (*Auth[T], error) {
|
||||||
keys, err := config.validate()
|
keys, err := config.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, refreshEnabled := keys[KIDRefresh]
|
_, refreshEnabled := keys[KIDRefresh]
|
||||||
|
|
||||||
s := &SSO[T]{
|
s := &Auth[T]{
|
||||||
config: config,
|
config: config,
|
||||||
keys: keys,
|
keys: keys,
|
||||||
securecookie: securecookie.New([]byte(config.Cookie.Hash), []byte(config.Cookie.Block)),
|
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
|
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 {
|
if provider != nil {
|
||||||
for i := range s.providers {
|
for i := range s.providers {
|
||||||
s.providers[i] = nil
|
s.providers[i] = nil
|
||||||
|
@ -108,11 +108,7 @@ func (s *SSO[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler Er
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSO[T]) AddProvider(providers ...Provider[T]) *SSO[T] {
|
func (s *Auth[T]) AddProvider(providers ...Provider[T]) *Auth[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...)
|
|
||||||
|
|
||||||
// A provider can also implement both transformer and
|
// A provider can also implement both transformer and
|
||||||
// error handler if that's the design option of the end-developer.
|
// error handler if that's the design option of the end-developer.
|
||||||
for _, p := range providers {
|
for _, p := range providers {
|
||||||
|
@ -137,22 +133,22 @@ func (s *SSO[T]) AddProvider(providers ...Provider[T]) *SSO[T] {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSO[T]) SetErrorHandler(errHandler ErrorHandler) *SSO[T] {
|
func (s *Auth[T]) SetErrorHandler(errHandler ErrorHandler) *Auth[T] {
|
||||||
s.errorHandler = errHandler
|
s.errorHandler = errHandler
|
||||||
return s
|
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
|
s.transformer = transformer
|
||||||
return s
|
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)
|
s.transformer = TransformerFunc[T](transfermerFunc)
|
||||||
return s
|
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
|
var t T
|
||||||
|
|
||||||
// get "t" from a valid provider.
|
// 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)
|
v, err := p.Signin(ctx, username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if i == n-1 { // last provider errored.
|
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.
|
// keep searching.
|
||||||
continue
|
continue
|
||||||
|
@ -174,19 +170,19 @@ func (s *SSO[T]) Signin(ctx stdContext.Context, username, password string) ([]by
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, nil, fmt.Errorf("sso: signin: no provider")
|
return nil, nil, fmt.Errorf("auth: signin: no provider")
|
||||||
}
|
}
|
||||||
|
|
||||||
// sign the tokens.
|
// sign the tokens.
|
||||||
accessToken, refreshToken, err := s.sign(t)
|
accessToken, refreshToken, err := s.sign(t)
|
||||||
if err != nil {
|
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
|
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.
|
// sign the tokens.
|
||||||
var (
|
var (
|
||||||
accessStdClaims StandardClaims
|
accessStdClaims StandardClaims
|
||||||
|
@ -239,7 +235,7 @@ func (s *SSO[T]) sign(t T) ([]byte, []byte, error) {
|
||||||
return accessToken, refreshToken, nil
|
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.
|
// No, let the developer decide it based on a middleware, e.g. iris.LimitRequestBodySize.
|
||||||
// ctx.SetMaxRequestBodySize(s.maxRequestBodySize)
|
// ctx.SetMaxRequestBodySize(s.maxRequestBodySize)
|
||||||
|
|
||||||
|
@ -283,16 +279,16 @@ func (s *SSO[T]) SigninHandler(ctx *context.Context) {
|
||||||
ctx.JSON(resp)
|
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)
|
t, claims, err := s.verify(ctx, token)
|
||||||
if err != nil {
|
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
|
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
|
var t T
|
||||||
|
|
||||||
if len(token) == 0 { // should never happen at this state.
|
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
|
return t, standardClaims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Good idea but not practical.
|
func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler {
|
||||||
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 {
|
|
||||||
return func(ctx *context.Context) {
|
return func(ctx *context.Context) {
|
||||||
accessToken := s.extractAccessToken(ctx)
|
accessToken := s.extractAccessToken(ctx)
|
||||||
|
|
||||||
|
@ -376,7 +356,7 @@ func (s *SSO[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = verify(t); err != nil {
|
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)
|
s.errorHandler.Unauthenticated(ctx, err)
|
||||||
return
|
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.
|
// first try from authorization: bearer header.
|
||||||
accessToken := extractTokenFromHeader(ctx)
|
accessToken := s.extractTokenFromHeader(ctx)
|
||||||
|
|
||||||
// then if no header, try try extract from cookie.
|
// then if no header, try try extract from cookie.
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
|
@ -408,27 +388,27 @@ func (s *SSO[T]) extractAccessToken(ctx *context.Context) string {
|
||||||
return accessToken
|
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 {
|
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)
|
t, _, err := s.verify(ctx, refreshToken)
|
||||||
if err != nil {
|
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
|
// refresh the tokens, both refresh & access tokens will be renew to prevent
|
||||||
// malicious 😈 users that may hold a refresh token.
|
// malicious 😈 users that may hold a refresh token.
|
||||||
accessTok, refreshTok, err := s.sign(t)
|
accessTok, refreshTok, err := s.sign(t)
|
||||||
if err != nil {
|
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
|
return accessTok, refreshTok, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSO[T]) RefreshHandler(ctx *context.Context) {
|
func (s *Auth[T]) RefreshHandler(ctx *context.Context) {
|
||||||
var req RefreshRequest
|
var req RefreshRequest
|
||||||
err := ctx.ReadJSON(&req)
|
err := ctx.ReadJSON(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -455,10 +435,10 @@ func (s *SSO[T]) RefreshHandler(ctx *context.Context) {
|
||||||
ctx.JSON(resp)
|
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)
|
t, standardClaims, err := s.verify(ctx, token)
|
||||||
if err != nil {
|
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++ {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSO[T]) SignoutHandler(ctx *context.Context) {
|
func (s *Auth[T]) SignoutHandler(ctx *context.Context) {
|
||||||
s.signoutHandler(ctx, false)
|
s.signoutHandler(ctx, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSO[T]) SignoutAllHandler(ctx *context.Context) {
|
func (s *Auth[T]) SignoutAllHandler(ctx *context.Context) {
|
||||||
s.signoutHandler(ctx, true)
|
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)
|
accessToken := s.extractAccessToken(ctx)
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing)
|
s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing)
|
||||||
|
@ -515,13 +495,8 @@ func (s *SSO[T]) signoutHandler(ctx *context.Context, all bool) {
|
||||||
ctx.Values().Remove(standardClaimsContextKey)
|
ctx.Values().Remove(standardClaimsContextKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
var headerKeys = [...]string{
|
func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string {
|
||||||
"Authorization",
|
for _, headerKey := range s.config.Headers {
|
||||||
"X-Authorization",
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractTokenFromHeader(ctx *context.Context) string {
|
|
||||||
for _, headerKey := range headerKeys {
|
|
||||||
headerValue := ctx.GetHeader(headerKey)
|
headerValue := ctx.GetHeader(headerKey)
|
||||||
if headerValue == "" {
|
if headerValue == "" {
|
||||||
continue
|
continue
|
||||||
|
@ -539,7 +514,7 @@ func extractTokenFromHeader(ctx *context.Context) string {
|
||||||
return ""
|
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 != "" {
|
if cookieName := s.config.Cookie.Name; cookieName != "" {
|
||||||
maxAge := s.keys[KIDAccess].MaxAge
|
maxAge := s.keys[KIDAccess].MaxAge
|
||||||
if maxAge == 0 {
|
if maxAge == 0 {
|
||||||
|
@ -551,6 +526,7 @@ func (s *SSO[T]) trySetCookie(ctx *context.Context, accessToken string) {
|
||||||
Name: cookieName,
|
Name: cookieName,
|
||||||
Value: url.QueryEscape(accessToken),
|
Value: url.QueryEscape(accessToken),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: ctx.IsSSL(),
|
||||||
Domain: ctx.Domain(),
|
Domain: ctx.Domain(),
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
Expires: time.Now().Add(maxAge),
|
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 != "" {
|
if cookieName := s.config.Cookie.Name; cookieName != "" {
|
||||||
ctx.RemoveCookie(cookieName)
|
ctx.RemoveCookie(cookieName)
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
//go:build go1.18
|
//go:build go1.18
|
||||||
|
|
||||||
package sso
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -16,50 +16,77 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KIDAccess = "IRIS_SSO_ACCESS"
|
// The JWT Key ID for access tokens.
|
||||||
KIDRefresh = "IRIS_SSO_REFRESH"
|
KIDAccess = "IRIS_AUTH_ACCESS"
|
||||||
|
// The JWT Key ID for refresh tokens.
|
||||||
|
KIDRefresh = "IRIS_AUTH_REFRESH"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// Configuration holds the necessary information for Iris Auth & Single-Sign-On feature.
|
||||||
|
//
|
||||||
|
// See the `New` package-level function.
|
||||||
Configuration struct {
|
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"`
|
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"`
|
Keys jwt.KeysConfiguration `json:"keys" yaml:"Keys" toml:"Keys" ini:"keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CookieConfiguration holds the necessary information for cookie client storage.
|
||||||
CookieConfiguration struct {
|
CookieConfiguration struct {
|
||||||
|
// Name defines the cookie's name.
|
||||||
Name string `json:"cookie" yaml:"Name" toml:"Name" ini:"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"`
|
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"`
|
Block string `json:"block" yaml:"Block" toml:"Block" ini:"block"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Configuration) validate() (jwt.Keys, error) {
|
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.Name != "" {
|
||||||
if c.Cookie.Hash == "" || c.Cookie.Block == "" {
|
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()
|
keys, err := c.Keys.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("sso: %w", err)
|
return nil, fmt.Errorf("auth: configuration: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := keys[KIDAccess]; !ok {
|
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.
|
// Let's keep refresh optional.
|
||||||
// if _, ok := keys[KIDRefresh]; !ok {
|
// 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
|
return keys, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BindRandom binds the "c" configuration to random values for keys and cookie security.
|
// BindRandom binds the "c" configuration to random values for keys and cookie security.
|
||||||
// Keys will not be persisted between restarts,
|
// 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 {
|
func (c *Configuration) BindRandom() error {
|
||||||
accessPublic, accessPrivate, err := jwt.GenerateEdDSA()
|
accessPublic, accessPrivate, err := jwt.GenerateEdDSA()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -72,8 +99,12 @@ func (c *Configuration) BindRandom() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
*c = Configuration{
|
*c = Configuration{
|
||||||
|
Headers: []string{
|
||||||
|
"Authorization",
|
||||||
|
"X-Authorization",
|
||||||
|
},
|
||||||
Cookie: CookieConfiguration{
|
Cookie: CookieConfiguration{
|
||||||
Name: "iris_sso",
|
Name: "iris_auth_cookie",
|
||||||
Hash: string(securecookie.GenerateRandomKey(64)),
|
Hash: string(securecookie.GenerateRandomKey(64)),
|
||||||
Block: string(securecookie.GenerateRandomKey(32)),
|
Block: string(securecookie.GenerateRandomKey(32)),
|
||||||
},
|
},
|
||||||
|
@ -99,6 +130,9 @@ func (c *Configuration) BindRandom() error {
|
||||||
return nil
|
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 {
|
func (c *Configuration) BindFile(filename string) error {
|
||||||
switch filepath.Ext(filename) {
|
switch filepath.Ext(filename) {
|
||||||
case ".json":
|
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) {
|
func (c *Configuration) ToYAML() ([]byte, error) {
|
||||||
return yaml.Marshal(c)
|
return yaml.Marshal(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToJSON returns the "c" Configuration's contents as raw json byte slice.
|
||||||
func (c *Configuration) ToJSON() ([]byte, error) {
|
func (c *Configuration) ToJSON() ([]byte, error) {
|
||||||
return json.Marshal(c)
|
return json.Marshal(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustGenerateConfiguration calls the Configuration's BindRandom
|
||||||
|
// method and returns the result. It panics on errors.
|
||||||
func MustGenerateConfiguration() (c Configuration) {
|
func MustGenerateConfiguration() (c Configuration) {
|
||||||
if err := c.BindRandom(); err != nil {
|
if err := c.BindRandom(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -147,11 +185,16 @@ func MustGenerateConfiguration() (c Configuration) {
|
||||||
return
|
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) {
|
func LoadConfiguration(filename string) (c Configuration, err error) {
|
||||||
err = c.BindFile(filename)
|
err = c.BindFile(filename)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MustLoadConfiguration same as LoadConfiguration package-level function
|
||||||
|
// but it panics on errors.
|
||||||
func MustLoadConfiguration(filename string) Configuration {
|
func MustLoadConfiguration(filename string) Configuration {
|
||||||
c, err := LoadConfiguration(filename)
|
c, err := LoadConfiguration(filename)
|
||||||
if err != nil {
|
if err != nil {
|
86
auth/provider.go
Normal file
86
auth/provider.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
//go:build go1.18
|
//go:build go1.18
|
||||||
|
|
||||||
package sso
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
|
@ -13,13 +13,13 @@ type (
|
||||||
User = interface{} // any 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 {
|
func GetAccessToken(ctx *context.Context) string {
|
||||||
return ctx.Values().GetString(accessTokenContextKey)
|
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 {
|
func GetStandardClaims(ctx *context.Context) StandardClaims {
|
||||||
if v := ctx.Values().Get(standardClaimsContextKey); v != nil {
|
if v := ctx.Values().Get(standardClaimsContextKey); v != nil {
|
||||||
|
@ -31,11 +31,11 @@ func GetStandardClaims(ctx *context.Context) StandardClaims {
|
||||||
return StandardClaims{}
|
return StandardClaims{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSO[T]) GetStandardClaims(ctx *context.Context) StandardClaims {
|
func (s *Auth[T]) GetStandardClaims(ctx *context.Context) StandardClaims {
|
||||||
return GetStandardClaims(ctx)
|
return GetStandardClaims(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContextKey = "iris.sso.context.user"
|
const userContextKey = "iris.auth.context.user"
|
||||||
|
|
||||||
func GetUser[T User](ctx *context.Context) T {
|
func GetUser[T User](ctx *context.Context) T {
|
||||||
if v := ctx.Values().Get(userContextKey); v != nil {
|
if v := ctx.Values().Get(userContextKey); v != nil {
|
||||||
|
@ -48,6 +48,6 @@ func GetUser[T User](ctx *context.Context) T {
|
||||||
return empty
|
return empty
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SSO[T]) GetUser(ctx *context.Context) T {
|
func (s *Auth[T]) GetUser(ctx *context.Context) T {
|
||||||
return GetUser[T](ctx)
|
return GetUser[T](ctx)
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user