1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-16 16:35:56 +00:00

rename the sso to auth package

This commit is contained in:
Gerasimos (Makis) Maropoulos
2022-04-02 17:30:55 +03:00
parent 60e19de9e2
commit 8652ee09f6
24 changed files with 233 additions and 205 deletions

544
auth/auth.go Normal file
View File

@@ -0,0 +1,544 @@
//go:build go1.18
package auth
import (
stdContext "context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/kataras/iris/v12/context"
"github.com/google/uuid"
"github.com/gorilla/securecookie"
"github.com/kataras/jwt"
)
type (
Auth[T User] struct {
config Configuration
keys jwt.Keys
securecookie context.SecureCookie
providers []Provider[T] // at least one.
errorHandler ErrorHandler
transformer Transformer[T]
claimsProvider ClaimsProvider
refreshEnabled bool // if KIDRefresh exists in keys.
}
TVerify[T User] func(t T) error
SigninRequest struct {
Username string `json:"username" form:"username,omitempty"` // username OR email, username has priority over email.
Email string `json:"email" form:"email,omitempty"` // email OR username.
Password string `json:"password" form:"password"`
}
SigninResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
}
RefreshRequest struct {
RefreshToken string `json:"refresh_token"`
}
)
func MustLoad[T User](filename string) *Auth[T] {
var config Configuration
if err := config.BindFile(filename); err != nil {
panic(err)
}
s, err := New[T](config)
if err != nil {
panic(err)
}
return s
}
func Must[T User](s *Auth[T], err error) *Auth[T] {
if err != nil {
panic(err)
}
return s
}
func New[T User](config Configuration) (*Auth[T], error) {
keys, err := config.validate()
if err != nil {
return nil, err
}
_, refreshEnabled := keys[KIDRefresh]
s := &Auth[T]{
config: config,
keys: keys,
securecookie: securecookie.New([]byte(config.Cookie.Hash), []byte(config.Cookie.Block)),
refreshEnabled: refreshEnabled,
// providers: []Provider[T]{newProvider[T]()},
errorHandler: new(DefaultErrorHandler),
}
return s, nil
}
func (s *Auth[T]) WithProviderAndErrorHandler(provider Provider[T], errHandler ErrorHandler) *Auth[T] {
if provider != nil {
for i := range s.providers {
s.providers[i] = nil
}
s.providers = nil
s.providers = make([]Provider[T], 0, 1)
s.AddProvider(provider)
}
if errHandler != nil {
s.SetErrorHandler(errHandler)
}
return s
}
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 {
if s.transformer == nil {
if transformer, ok := p.(Transformer[T]); ok {
s.SetTransformer(transformer)
}
}
if errHandler, ok := p.(ErrorHandler); ok {
s.SetErrorHandler(errHandler)
}
if s.claimsProvider == nil {
if claimsProvider, ok := p.(ClaimsProvider); ok {
s.claimsProvider = claimsProvider
}
}
}
s.providers = append(s.providers, providers...)
return s
}
func (s *Auth[T]) SetErrorHandler(errHandler ErrorHandler) *Auth[T] {
s.errorHandler = errHandler
return s
}
func (s *Auth[T]) SetTransformer(transformer Transformer[T]) *Auth[T] {
s.transformer = transformer
return s
}
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 *Auth[T]) Signin(ctx stdContext.Context, username, password string) ([]byte, []byte, error) {
var t T
// get "t" from a valid provider.
if n := len(s.providers); n > 0 {
for i := 0; i < n; i++ {
p := s.providers[i]
v, err := p.Signin(ctx, username, password)
if err != nil {
if i == n-1 { // last provider errored.
return nil, nil, fmt.Errorf("auth: signin: %w", err)
}
// keep searching.
continue
}
// found.
t = v
break
}
} else {
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("auth: signin: %w", err)
}
return accessToken, refreshToken, nil
}
func (s *Auth[T]) sign(t T) ([]byte, []byte, error) {
// sign the tokens.
var (
accessStdClaims StandardClaims
refreshStdClaims StandardClaims
)
if s.claimsProvider != nil {
accessStdClaims = s.claimsProvider.GetAccessTokenClaims()
refreshStdClaims = s.claimsProvider.GetRefreshTokenClaims(accessStdClaims)
}
iat := jwt.Clock().Unix()
if accessStdClaims.IssuedAt == 0 {
accessStdClaims.IssuedAt = iat
}
if accessStdClaims.ID == "" {
accessStdClaims.ID = uuid.NewString()
}
if refreshStdClaims.IssuedAt == 0 {
refreshStdClaims.IssuedAt = iat
}
if refreshStdClaims.ID == "" {
refreshStdClaims.ID = uuid.NewString()
}
if refreshStdClaims.OriginID == "" {
// keep a reference of the access token the refresh token is created,
// if that access token is invalidated then
// its refresh token should be too so the user can force-login.
refreshStdClaims.OriginID = accessStdClaims.ID
}
accessToken, err := s.keys.SignToken(KIDAccess, t, accessStdClaims)
if err != nil {
return nil, nil, fmt.Errorf("access: %w", err)
}
var refreshToken []byte
if s.refreshEnabled {
refreshToken, err = s.keys.SignToken(KIDRefresh, t, refreshStdClaims)
if err != nil {
return nil, nil, fmt.Errorf("refresh: %w", err)
}
}
return accessToken, refreshToken, nil
}
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)
var (
req SigninRequest
err error
)
switch ctx.GetContentTypeRequested() {
case context.ContentFormHeaderValue, context.ContentFormMultipartHeaderValue:
err = ctx.ReadForm(&req)
default:
err = ctx.ReadJSON(&req)
}
if err != nil {
s.errorHandler.InvalidArgument(ctx, err)
return
}
if req.Username == "" {
req.Username = req.Email
}
accessTokenBytes, refreshTokenBytes, err := s.Signin(ctx, req.Username, req.Password)
if err != nil {
s.tryRemoveCookie(ctx) // remove cookie on invalidated.
s.errorHandler.Unauthenticated(ctx, err)
return
}
accessToken := jwt.BytesToString(accessTokenBytes)
refreshToken := jwt.BytesToString(refreshTokenBytes)
s.trySetCookie(ctx, accessToken)
resp := SigninResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
}
ctx.JSON(resp)
}
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("auth: verify: %w", err)
}
return t, claims, nil
}
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.
return t, StandardClaims{}, jwt.ErrMissing
}
verifiedToken, err := jwt.VerifyWithHeaderValidator(nil, nil, token, s.keys.ValidateHeader, jwt.Leeway(time.Minute))
if err != nil {
return t, StandardClaims{}, err
}
if s.transformer != nil {
if t, err = s.transformer.Transform(ctx, verifiedToken); err != nil {
return t, StandardClaims{}, err
}
} else {
if err = verifiedToken.Claims(&t); err != nil {
return t, StandardClaims{}, err
}
}
standardClaims := verifiedToken.StandardClaims
if n := len(s.providers); n > 0 {
for i := 0; i < n; i++ {
p := s.providers[i]
err := p.ValidateToken(ctx, standardClaims, t)
if err != nil {
if i == n-1 { // last provider errored.
return t, StandardClaims{}, err
}
// keep searching.
continue
}
// token is allowed.
break
}
} else {
// return t, StandardClaims{}, fmt.Errorf("no provider")
}
return t, standardClaims, nil
}
func (s *Auth[T]) VerifyHandler(verifyFuncs ...TVerify[T]) context.Handler {
return func(ctx *context.Context) {
accessToken := s.extractAccessToken(ctx)
if accessToken == "" { // if empty, fire 401.
s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing)
return
}
t, claims, err := s.Verify(ctx, []byte(accessToken))
if err != nil {
s.errorHandler.Unauthenticated(ctx, err)
return
}
for _, verify := range verifyFuncs {
if verify == nil {
continue
}
if err = verify(t); err != nil {
err = fmt.Errorf("auth: verify: %v", err)
s.errorHandler.Unauthenticated(ctx, err)
return
}
}
ctx.SetUser(t)
// store the user to the request.
ctx.Values().Set(accessTokenContextKey, accessToken)
ctx.Values().Set(userContextKey, t)
ctx.Values().Set(standardClaimsContextKey, claims)
ctx.Next()
}
}
func (s *Auth[T]) extractAccessToken(ctx *context.Context) string {
// first try from authorization: bearer header.
accessToken := s.extractTokenFromHeader(ctx)
// then if no header, try try extract from cookie.
if accessToken == "" {
if cookieName := s.config.Cookie.Name; cookieName != "" {
accessToken = ctx.GetCookie(cookieName, context.CookieEncoding(s.securecookie))
}
}
return accessToken
}
func (s *Auth[T]) Refresh(ctx stdContext.Context, refreshToken []byte) ([]byte, []byte, error) {
if !s.refreshEnabled {
return nil, nil, fmt.Errorf("auth: refresh: disabled")
}
t, _, err := s.verify(ctx, refreshToken)
if err != nil {
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("auth: refresh: %w", err)
}
return accessTok, refreshTok, nil
}
func (s *Auth[T]) RefreshHandler(ctx *context.Context) {
var req RefreshRequest
err := ctx.ReadJSON(&req)
if err != nil {
s.errorHandler.InvalidArgument(ctx, err)
return
}
accessTokenBytes, refreshTokenBytes, err := s.Refresh(ctx, []byte(req.RefreshToken))
if err != nil {
// s.tryRemoveCookie(ctx)
s.errorHandler.Unauthenticated(ctx, err)
return
}
accessToken := jwt.BytesToString(accessTokenBytes)
refreshToken := jwt.BytesToString(refreshTokenBytes)
s.trySetCookie(ctx, accessToken)
resp := SigninResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
}
ctx.JSON(resp)
}
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("auth: signout: verify: %w", err)
}
for i, n := 0, len(s.providers)-1; i <= n; i++ {
p := s.providers[i]
if all {
err = p.InvalidateTokens(ctx, t)
} else {
err = p.InvalidateToken(ctx, standardClaims, t)
}
if err != nil {
if i == n { // last provider errored.
return err
}
// keep trying.
continue
}
// token is marked as invalidated by a provider.
break
}
return nil
}
func (s *Auth[T]) SignoutHandler(ctx *context.Context) {
s.signoutHandler(ctx, false)
}
func (s *Auth[T]) SignoutAllHandler(ctx *context.Context) {
s.signoutHandler(ctx, true)
}
func (s *Auth[T]) signoutHandler(ctx *context.Context, all bool) {
accessToken := s.extractAccessToken(ctx)
if accessToken == "" {
s.errorHandler.Unauthenticated(ctx, jwt.ErrMissing)
return
}
err := s.Signout(ctx, []byte(accessToken), all)
if err != nil {
s.errorHandler.Unauthenticated(ctx, err)
return
}
s.tryRemoveCookie(ctx)
ctx.SetUser(nil)
ctx.Values().Remove(accessTokenContextKey)
ctx.Values().Remove(userContextKey)
ctx.Values().Remove(standardClaimsContextKey)
}
func (s *Auth[T]) extractTokenFromHeader(ctx *context.Context) string {
for _, headerKey := range s.config.Headers {
headerValue := ctx.GetHeader(headerKey)
if headerValue == "" {
continue
}
// pure check: authorization header format must be Bearer {token}
authHeaderParts := strings.Split(headerValue, " ")
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
continue
}
return authHeaderParts[1]
}
return ""
}
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 {
maxAge = context.SetCookieKVExpiration
}
cookie := &http.Cookie{
Path: "/",
Name: cookieName,
Value: url.QueryEscape(accessToken),
HttpOnly: true,
Secure: ctx.IsSSL(),
Domain: ctx.Domain(),
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(maxAge),
MaxAge: int(maxAge.Seconds()),
}
ctx.SetCookie(cookie, context.CookieEncoding(s.securecookie))
}
}
func (s *Auth[T]) tryRemoveCookie(ctx *context.Context) {
if cookieName := s.config.Cookie.Name; cookieName != "" {
ctx.RemoveCookie(cookieName)
}
}

205
auth/configuration.go Normal file
View File

@@ -0,0 +1,205 @@
//go:build go1.18
package auth
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/gorilla/securecookie"
"github.com/kataras/jwt"
"gopkg.in/yaml.v3"
)
const (
// 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"`
// 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 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("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("auth: configuration: %w", err)
}
if _, ok := keys[KIDAccess]; !ok {
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("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,
// see BindFile method and LoadConfiguration/MustLoadConfiguration package-level functions.
func (c *Configuration) BindRandom() error {
accessPublic, accessPrivate, err := jwt.GenerateEdDSA()
if err != nil {
return err
}
refreshPublic, refreshPrivate, err := jwt.GenerateEdDSA()
if err != nil {
return err
}
*c = Configuration{
Headers: []string{
"Authorization",
"X-Authorization",
},
Cookie: CookieConfiguration{
Name: "iris_auth_cookie",
Hash: string(securecookie.GenerateRandomKey(64)),
Block: string(securecookie.GenerateRandomKey(32)),
},
Keys: jwt.KeysConfiguration{
{
ID: KIDAccess,
Alg: jwt.EdDSA.Name(),
MaxAge: 2 * time.Hour,
Public: string(accessPublic),
Private: string(accessPrivate),
},
{
ID: KIDRefresh,
Alg: jwt.EdDSA.Name(),
MaxAge: 720 * time.Hour,
Public: string(refreshPublic),
Private: string(refreshPrivate),
EncryptionKey: string(jwt.MustGenerateRandom(32)),
},
},
}
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":
contents, err := os.ReadFile(filename)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
generatedConfig := MustGenerateConfiguration()
if generatedYAML, gErr := generatedConfig.ToJSON(); gErr == nil {
err = fmt.Errorf("%w: example:\n\n%s", err, generatedYAML)
}
}
return err
}
return json.Unmarshal(contents, c)
default:
contents, err := os.ReadFile(filename)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
generatedConfig := MustGenerateConfiguration()
if generatedYAML, gErr := generatedConfig.ToYAML(); gErr == nil {
err = fmt.Errorf("%w: example:\n\n%s", err, generatedYAML)
}
}
return err
}
return yaml.Unmarshal(contents, c)
}
}
// 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)
}
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 {
panic(err)
}
return c
}

86
auth/provider.go Normal file
View 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)
}

53
auth/user.go Normal file
View File

@@ -0,0 +1,53 @@
//go:build go1.18
package auth
import (
"github.com/kataras/iris/v12/context"
"github.com/kataras/jwt"
)
type (
StandardClaims = jwt.Claims
User = interface{} // any type.
)
const accessTokenContextKey = "iris.auth.context.access_token"
func GetAccessToken(ctx *context.Context) string {
return ctx.Values().GetString(accessTokenContextKey)
}
const standardClaimsContextKey = "iris.auth.context.standard_claims"
func GetStandardClaims(ctx *context.Context) StandardClaims {
if v := ctx.Values().Get(standardClaimsContextKey); v != nil {
if c, ok := v.(StandardClaims); ok {
return c
}
}
return StandardClaims{}
}
func (s *Auth[T]) GetStandardClaims(ctx *context.Context) StandardClaims {
return GetStandardClaims(ctx)
}
const userContextKey = "iris.auth.context.user"
func GetUser[T User](ctx *context.Context) T {
if v := ctx.Values().Get(userContextKey); v != nil {
if t, ok := v.(T); ok {
return t
}
}
var empty T
return empty
}
func (s *Auth[T]) GetUser(ctx *context.Context) T {
return GetUser[T](ctx)
}