Files
go-omni/auth/auth.go

147 lines
4.3 KiB
Go

// 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
}