// Package auth defines authentication and authorization boundaries for GoOmni. // It does not choose JWT, sessions, OAuth, or any other credential format. package auth import ( "context" "errors" "strings" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" "git.yuntuo.hk/k2safe/go-omni/httpx" ) const principalKey = "go-omni.auth.principal" var ( ErrMissingToken = errors.New("authorization token is missing") ErrInvalidToken = errors.New("authorization token is invalid") ) // Principal is the authenticated identity made available to business handlers. // Claims are optional provider-specific values and should not be required by core business code. type Principal struct { UserID string `json:"userId"` TenantID string `json:"tenantId,omitempty"` Roles []string `json:"roles,omitempty"` Claims map[string]any `json:"claims,omitempty"` } func (p Principal) HasRole(role string) bool { role = strings.TrimSpace(role) if role == "" { return false } for _, candidate := range p.Roles { if candidate == role { return true } } return false } // Authenticator converts a credential into a provider-neutral Principal. type Authenticator interface { Authenticate(context.Context, string) (Principal, error) } // StaticAuthenticator is intended only for local development and tests. type StaticAuthenticator map[string]Principal func (a StaticAuthenticator) Authenticate(_ context.Context, token string) (Principal, error) { principal, ok := a[token] if !ok { return Principal{}, ErrInvalidToken } return clonePrincipal(principal), nil } // FromContext returns the authenticated identity attached by RequireAuth. func FromContext(ctx *httpx.Context) (Principal, bool) { value, ok := ctx.Get(principalKey) if !ok { return Principal{}, false } principal, ok := value.(Principal) return principal, ok } // RequireAuth extracts a Bearer token and attaches its Principal to the request. func RequireAuth(authenticator Authenticator) httpx.Middleware { return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) error { if authenticator == nil { return apierrors.Internal(errors.New("authenticator is not configured")) } token, err := bearerToken(ctx.Header("Authorization")) if err != nil { return apierrors.Unauthorized("UNAUTHENTICATED", "authentication is required").WithCause(err) } principal, err := authenticator.Authenticate(ctx.Request().Context(), token) if err != nil || strings.TrimSpace(principal.UserID) == "" { if err == nil { err = ErrInvalidToken } return apierrors.Unauthorized("UNAUTHENTICATED", "authentication is required").WithCause(err) } ctx.Set(principalKey, clonePrincipal(principal)) return next(ctx) } } } // OptionalAuth attaches a Principal when a valid Bearer token is present. // Missing credentials are allowed; malformed or invalid credentials are rejected. func OptionalAuth(authenticator Authenticator) httpx.Middleware { return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) error { if strings.TrimSpace(ctx.Header("Authorization")) == "" { return next(ctx) } return RequireAuth(authenticator)(next)(ctx) } } } // RequireRole checks a Principal attached by RequireAuth. func RequireRole(roles ...string) httpx.Middleware { return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) error { principal, ok := FromContext(ctx) if !ok { return apierrors.Unauthorized("UNAUTHENTICATED", "authentication is required") } for _, role := range roles { if principal.HasRole(role) { return next(ctx) } } return apierrors.Forbidden("FORBIDDEN", "permission denied") } } } func bearerToken(value string) (string, error) { parts := strings.Fields(value) if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") || parts[1] == "" { return "", ErrMissingToken } return parts[1], nil } func clonePrincipal(principal Principal) Principal { principal.Roles = append([]string(nil), principal.Roles...) if principal.Claims != nil { principal.Claims = mapsClone(principal.Claims) } return principal } func mapsClone(values map[string]any) map[string]any { clone := make(map[string]any, len(values)) for key, value := range values { clone[key] = value } return clone }