Documentation
¶
Overview ¶
Package iam provides identity and access management services for Grid API.
The IAM service centralizes all authentication, authorization, session management, and role resolution logic. It provides:
- Authentication via multiple strategies (JWT, Session cookies)
- Immutable group→role cache with lock-free reads
- Read-only Casbin policy evaluation
- Session lifecycle management
- User and service account management
Architecture:
- Authenticator interface: Pluggable authentication strategies
- Principal struct: Unified authentication result (immutable)
- GroupRoleCache: Atomic snapshot cache for group→role mappings
- Service interface: Facade for all IAM operations
Request Flow:
Request → MultiAuth → Authenticator.Authenticate() → Principal (with Roles)
↓
Handler → IAM.Authorize(principal) → Casbin (read-only)
The key design principle is that roles are resolved ONCE at authentication time and stored in the Principal struct. Authorization then uses these pre-resolved roles without mutating any shared state.
Index ¶
- func AuthorizeWithRoles(enforcer casbin.IEnforcer, roles []string, obj, act string, ...) (bool, error)
- type AuthRequest
- type Authenticator
- type GroupRoleCache
- type GroupRoleSnapshot
- type IAMServiceConfig
- type IAMServiceDependencies
- type JWTAuthenticator
- type Principal
- type PrincipalType
- type Service
- type SessionAuthenticator
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AuthorizeWithRoles ¶
func AuthorizeWithRoles( enforcer casbin.IEnforcer, roles []string, obj, act string, labels map[string]interface{}, ) (bool, error)
AuthorizeWithRoles checks if ANY of the principal's roles grants permission for the requested action.
This is a READ-ONLY authorization check that never mutates Casbin state. It queries the enforcer with each role in the principal's role list and returns true if at least one role allows the action.
The enforcer is queried with role principals (e.g., "role:product-engineer") which are defined in the static Casbin policy. This eliminates the need for dynamic user→group→role mappings.
Parameters:
- enforcer: Casbin enforcer loaded with static role-based policies
- roles: List of role names (without "role:" prefix) assigned to the principal
- obj: Object type (e.g., "state", "admin", "policy") or specific resource ID
- act: Action being requested (e.g., "state:create", "state:read", "admin:role:manage")
- labels: Resource-specific attributes for label-based filtering (e.g., state labels)
Returns:
- bool: true if at least one role grants permission, false otherwise
- error: Any error during enforcement (e.g., enforcer failure, invalid policy)
Thread Safety:
- This function is thread-safe and performs no state mutation
- Multiple goroutines can call this concurrently without locks
Performance:
- O(n) where n = number of roles (typically 1-5 roles per user)
- Each enforcer.Enforce() call is fast (cached policy evaluation)
- No database queries or blocking I/O
Example:
roles := []string{"product-engineer", "viewer"}
labels := map[string]interface{}{"env": "dev"}
allowed, err := AuthorizeWithRoles(enforcer, roles, "state", "state:read", labels)
if err != nil {
return fmt.Errorf("authorization error: %w", err)
}
if !allowed {
return fmt.Errorf("permission denied")
}
Types ¶
type AuthRequest ¶
type AuthRequest struct {
// Headers contains HTTP headers (including Authorization, Cookie)
Headers http.Header
// Cookies contains parsed cookies
Cookies []*http.Cookie
}
AuthRequest wraps HTTP request data for authenticator implementations. This abstraction allows authenticators to work with both HTTP and Connect RPC requests.
type Authenticator ¶
type Authenticator interface {
// Authenticate validates credentials and returns a Principal with resolved roles.
Authenticate(ctx context.Context, req AuthRequest) (*Principal, error)
}
Authenticator validates credentials and returns a Principal with resolved roles.
Implementations:
- JWTAuthenticator: Validates Bearer tokens (internal or external IdP)
- SessionAuthenticator: Validates session cookies
Return values:
- (principal, nil): Authentication successful
- (nil, nil): Credentials not present (not an error, try next authenticator)
- (nil, error): Authentication failed (invalid credentials)
The authenticator is responsible for:
- Extracting credentials from request
- Validating credentials (signature, expiry, revocation)
- Resolving identity (user or service account)
- Computing effective roles (user_roles ∪ group_roles)
- Constructing immutable Principal struct
type GroupRoleCache ¶
type GroupRoleCache struct {
// contains filtered or unexported fields
}
GroupRoleCache provides lock-free access to group→role mappings.
Uses atomic.Value for zero-contention reads. The cache stores immutable snapshots that are never modified after creation. Refresh operations build a new snapshot and atomically swap the pointer.
This eliminates the race condition in the old implementation where concurrent requests would mutate shared Casbin state.
func NewGroupRoleCache ¶
func NewGroupRoleCache(groupRoleRepo repository.GroupRoleRepository, roleRepo repository.RoleRepository) (*GroupRoleCache, error)
NewGroupRoleCache creates a new cache and performs initial load from database.
Returns error if initial load fails (e.g., database unavailable). The cache must be successfully initialized before the server can start.
func (*GroupRoleCache) Get ¶
func (c *GroupRoleCache) Get() *GroupRoleSnapshot
Get returns the current snapshot for lock-free reads.
This method never blocks and has O(1) latency. Safe for concurrent access from unlimited goroutines with zero contention.
Returns nil if cache has never been loaded (should never happen after successful NewGroupRoleCache call).
func (*GroupRoleCache) GetRolesForGroups ¶
func (c *GroupRoleCache) GetRolesForGroups(groups []string) []string
GetRolesForGroups computes the union of roles for the given groups.
This is a PURE FUNCTION with no side effects:
- No database queries
- No state mutation
- Uses lock-free cache read (Get())
Returns deduplicated list of role names. If a role is granted by multiple groups, it appears once in the result.
Examples:
- GetRolesForGroups(["platform-engineers"]) → ["platform-engineer"]
- GetRolesForGroups(["platform-engineers", "dev-team"]) → ["platform-engineer", "product-engineer"]
- GetRolesForGroups([]) → []
- GetRolesForGroups(["unknown-group"]) → []
func (*GroupRoleCache) Refresh ¶
func (c *GroupRoleCache) Refresh(ctx context.Context) error
Refresh rebuilds the cache from database and atomically swaps the snapshot.
This is an out-of-band operation (not in request path). It's safe to call concurrently with Get() - readers will see either the old or new snapshot atomically, never a partial update.
Called by:
- Server startup (NewGroupRoleCache)
- Background refresh goroutine (every N minutes)
- Admin API (manual refresh)
- After AssignGroupRole/RemoveGroupRole
Performance: Typically 50-100ms depending on database latency and number of mappings. Not a concern since this runs out-of-band.
type GroupRoleSnapshot ¶
type GroupRoleSnapshot struct {
// Mappings: groupName → []roleName
// Example: {"platform-engineers": ["platform-engineer"], "dev-team": ["product-engineer"]}
Mappings map[string][]string
// CreatedAt is when this snapshot was built.
CreatedAt time.Time
// Version is an incrementing counter for debugging.
// Helps identify which snapshot is active.
Version int
}
GroupRoleSnapshot is an immutable snapshot of group→role mappings.
Stored in atomic.Value for lock-free reads. Never modified after creation. To update, create a new snapshot and atomically swap the pointer.
type IAMServiceConfig ¶
IAMServiceConfig contains configuration for IAM service construction. Separated from dependencies to clearly distinguish config from runtime dependencies.
type IAMServiceDependencies ¶
type IAMServiceDependencies struct {
Users repository.UserRepository
ServiceAccounts repository.ServiceAccountRepository
Sessions repository.SessionRepository
UserRoles repository.UserRoleRepository
GroupRoles repository.GroupRoleRepository
Roles repository.RoleRepository
RevokedJTIs repository.RevokedJTIRepository
Enforcer casbin.IEnforcer
}
IAMServiceDependencies contains all dependencies for IAM service construction.
This struct is used for dependency injection, making it easy to:
- Test with mocks
- Swap implementations
- Add new dependencies without breaking existing code
type JWTAuthenticator ¶
type JWTAuthenticator struct {
// contains filtered or unexported fields
}
JWTAuthenticator authenticates requests using JWT bearer tokens.
Implementation follows Phase 3 specification:
- Extract "Authorization: Bearer <token>" header
- Return (nil, nil) if not present
- Verify JWT signature using existing auth.Verifier logic
- Extract claims: sub, email, groups, jti
- Check JTI revocation
- Resolve user/service account (JIT provision if needed)
- Call ResolveRoles() using immutable cache
- Construct Principal with all fields populated
- Return Principal
This authenticator is stateless and thread-safe.
func NewJWTAuthenticator ¶
func NewJWTAuthenticator( cfg *config.Config, users repository.UserRepository, serviceAccounts repository.ServiceAccountRepository, revokedJTIs repository.RevokedJTIRepository, iamService Service, ) (*JWTAuthenticator, error)
NewJWTAuthenticator creates a new JWT authenticator.
This constructor initializes the OIDC token handler using the same logic as auth.NewVerifier, but adapted for the Authenticator interface pattern.
func (*JWTAuthenticator) Authenticate ¶
func (a *JWTAuthenticator) Authenticate(ctx context.Context, req AuthRequest) (*Principal, error)
Authenticate extracts and validates JWT bearer tokens.
Returns:
- (nil, nil) if no Authorization header present (no credentials for this authenticator)
- (nil, error) if authentication fails (invalid token, revoked JTI, etc.)
- (*Principal, nil) if authentication succeeds
type Principal ¶
type Principal struct {
// Subject is the stable OIDC subject or client identifier (unprefixed).
// Examples: "[email protected]", "sa:019a1234-5678-7abc-def0-123456789abc"
Subject string
// PrincipalID is the Casbin-ready identifier with type prefix.
// Examples: "user:[email protected]", "sa:019a1234-..."
// Used for Casbin policy evaluation.
PrincipalID string
// InternalID references the backing database record.
// For users: users.id (UUID)
// For service accounts: service_accounts.id (UUID)
InternalID string
// Email is the user's email address (optional, only for human users).
Email string
// Name is the display name (optional, only for human users).
Name string
// SessionID references the active session (optional, only for cookie auth).
// References sessions.id (UUID).
SessionID string
// Groups lists the IdP groups this principal belongs to.
// Examples: ["platform-engineers", "product-engineers"]
// Source: JWT claims (external IdP) or session.id_token (stored JWT).
Groups []string
// Roles lists the resolved role names (NOT Casbin role IDs).
// Examples: ["platform-engineer", "product-engineer"]
// Computed as: user_roles ∪ group_roles (via immutable cache).
//
// This is the SOURCE OF TRUTH for authorization decisions.
// Authorization checks iterate over these roles and query Casbin
// policies without mutating any state.
Roles []string
// Type differentiates users and service accounts.
Type PrincipalType
}
Principal represents an authenticated identity with pre-resolved roles.
This struct is IMMUTABLE after construction. Roles are computed once at authentication time and never modified. This eliminates race conditions caused by shared mutable state.
The Principal is stored in request context and used by authorization middleware to make access control decisions.
type PrincipalType ¶
type PrincipalType string
PrincipalType identifies whether this is a user or service account.
const ( // PrincipalTypeUser represents a human user (authenticated via password or SSO). PrincipalTypeUser PrincipalType = "user" // PrincipalTypeServiceAccount represents a machine identity (authenticated via client credentials). PrincipalTypeServiceAccount PrincipalType = "service_account" )
type Service ¶
type Service interface {
// AuthenticateRequest tries all registered authenticators in order.
// Returns the first successful Principal, or nil if none succeed.
//
// Authenticators are tried in priority order:
// 1. SessionAuthenticator (checks cookie)
// 2. JWTAuthenticator (checks Bearer token)
//
// Returns:
// - (principal, nil): Authentication successful
// - (nil, nil): No valid credentials found (unauthenticated request)
// - (nil, error): Authentication failed (invalid credentials)
AuthenticateRequest(ctx context.Context, req AuthRequest) (*Principal, error)
// ResolveRoles computes effective roles for a principal.
//
// This is a PURE FUNCTION with no side effects:
// - No database writes
// - No Casbin mutation
// - Uses immutable group→role cache for lock-free reads
//
// Parameters:
// - principalID: users.id or service_accounts.id (UUID)
// - groups: Group names from JWT/session
// - isUser: true for users, false for service accounts
//
// Returns: user_roles ∪ group_roles (union, deduplicated)
ResolveRoles(ctx context.Context, principalID string, groups []string, isUser bool) ([]string, error)
// Authorize checks if principal has permission for action on object with labels.
//
// Uses principal.Roles (pre-resolved at authentication time) to check
// against Casbin policies. NO Casbin state mutation occurs.
//
// For each role in principal.Roles:
// - Query Casbin policy: enforcer.Enforce(roleID, obj, act, labels)
// - If ANY role allows: return true
//
// This is READ-ONLY - no AddGroupingPolicy or similar calls.
Authorize(ctx context.Context, principal *Principal, obj, act string, labels map[string]interface{}) (bool, error)
// RefreshGroupRoleCache reloads group→role mappings from database.
//
// Uses atomic.Value.Store() for zero-downtime hot-reload:
// 1. Build new map from DB (group_roles + roles tables)
// 2. Create immutable snapshot
// 3. Atomically swap pointer (old readers unaffected)
//
// Called by:
// - Server startup (initial load)
// - Background goroutine (periodic refresh, e.g., every 5 minutes)
// - Admin API (manual refresh)
// - After AssignGroupRole/RemoveGroupRole (automatic refresh)
RefreshGroupRoleCache(ctx context.Context) error
// GetGroupRoleCacheSnapshot returns the current cache snapshot for debugging.
// Contains: map[groupName][]roleName, version, timestamp
GetGroupRoleCacheSnapshot() GroupRoleSnapshot
// CreateSession creates a new session after successful authentication.
//
// Parameters:
// - userID: users.id (UUID)
// - idToken: JWT from external IdP (stored for group extraction)
// - expiresAt: Session expiry time
//
// Returns:
// - session: Created session record
// - token: Unhashed session token (set as cookie)
// - error: If creation fails
//
// The token is hashed (SHA256) before storage for security.
CreateSession(ctx context.Context, userID, idToken string, expiresAt time.Time) (*models.Session, string, error)
// RevokeSession invalidates a session by ID.
// Sets session.revoked = true and session.revoked_at = now().
RevokeSession(ctx context.Context, sessionID string) error
// GetSessionByID retrieves a session by its ID.
// Returns repository.ErrNotFound if session doesn't exist.
GetSessionByID(ctx context.Context, sessionID string) (*models.Session, error)
// ListUserSessions retrieves all sessions for a specific user.
// Returns empty slice if user has no active sessions.
ListUserSessions(ctx context.Context, userID string) ([]models.Session, error)
// RevokeJTI adds a JWT ID to the revocation list.
// Used for logout and emergency token revocation.
RevokeJTI(ctx context.Context, jti string, expiresAt time.Time) error
// CreateUser creates a new user (internal or external IdP).
//
// Parameters:
// - email: User's email address (required)
// - username: Display name (required)
// - subject: OIDC subject from external IdP (optional, for Mode 1)
// - passwordHash: bcrypt password hash (optional, for Mode 2 internal users)
//
// Used by:
// - SSO callback handler (JIT provisioning with subject)
// - gridapi users create CLI command (internal users with passwordHash)
CreateUser(ctx context.Context, email, username, subject, passwordHash string) (*models.User, error)
// GetUserByEmail retrieves user by email address.
// Returns repository.ErrNotFound if user doesn't exist.
GetUserByEmail(ctx context.Context, email string) (*models.User, error)
// GetUserBySubject retrieves user by OIDC subject.
// Returns repository.ErrNotFound if user doesn't exist.
GetUserBySubject(ctx context.Context, subject string) (*models.User, error)
// GetUserByID retrieves user by internal ID.
// Returns repository.ErrNotFound if user doesn't exist.
GetUserByID(ctx context.Context, userID string) (*models.User, error)
// DisableUser sets user.disabled = true.
// Disabled users cannot authenticate.
DisableUser(ctx context.Context, userID string) error
// CreateServiceAccount creates a new service account for machine-to-machine auth.
//
// Returns:
// - serviceAccount: Created record
// - clientSecret: Unhashed secret (return to caller, not stored)
//
// The secret is hashed (bcrypt) before storage.
CreateServiceAccount(ctx context.Context, name, createdBy string) (*models.ServiceAccount, string, error)
// ListServiceAccounts returns all service accounts.
ListServiceAccounts(ctx context.Context) ([]*models.ServiceAccount, error)
// GetServiceAccountByName retrieves a service account by its human-readable name.
// Returns repository.ErrNotFound if the service account doesn't exist.
GetServiceAccountByName(ctx context.Context, name string) (*models.ServiceAccount, error)
// GetServiceAccountByClientID retrieves a service account by its client ID.
GetServiceAccountByClientID(ctx context.Context, clientID string) (*models.ServiceAccount, error)
// GetServiceAccountByID retrieves a service account by its internal ID.
// Returns repository.ErrNotFound if service account doesn't exist.
GetServiceAccountByID(ctx context.Context, saID string) (*models.ServiceAccount, error)
// RevokeServiceAccount disables a service account and cleans up all associated resources.
// This is an out-of-band mutation operation that:
// - Sets disabled = true in the database
// - Revokes all active sessions for the service account
// - Removes all Casbin role assignments for the service account
RevokeServiceAccount(ctx context.Context, clientID string) error
// RotateServiceAccountSecret generates a new secret for a service account.
// Returns the unhashed secret (caller must save it) and the timestamp of rotation.
// The secret is hashed with bcrypt before storage.
RotateServiceAccountSecret(ctx context.Context, clientID string) (string, time.Time, error)
// AssignRolesToServiceAccount assigns one or more roles to a service account.
//
// This method wraps AssignUserRole for convenience and ensures cache refresh
// semantics remain centralized.
AssignRolesToServiceAccount(ctx context.Context, serviceAccountID string, roleIDs []string) error
// RemoveRolesFromServiceAccount removes one or more roles from a service account.
RemoveRolesFromServiceAccount(ctx context.Context, serviceAccountID string, roleIDs []string) error
// AssignUserRole assigns a role directly to a user or service account.
//
// Exactly one of userID or serviceAccountID must be provided (non-empty string).
// This creates a UserRole record and syncs to Casbin.
//
// This is an out-of-band mutation (admin operation), not part of the auth request path.
AssignUserRole(ctx context.Context, userID, serviceAccountID, roleID string) error
// RemoveUserRole removes a role from a user or service account.
//
// Exactly one of userID or serviceAccountID must be provided (non-empty string).
// This deletes the UserRole record and removes from Casbin.
//
// This is an out-of-band mutation (admin operation), not part of the auth request path.
RemoveUserRole(ctx context.Context, userID, serviceAccountID, roleID string) error
// AssignGroupRole assigns a role to an IdP group.
//
// After persisting to database, automatically calls RefreshGroupRoleCache()
// to ensure new mappings are visible to authentication flow immediately.
//
// This is a control-plane operation (not in request path), so the cache
// refresh latency is acceptable.
AssignGroupRole(ctx context.Context, groupName, roleID string) error
// RemoveGroupRole removes a role from a group.
//
// After deletion, automatically calls RefreshGroupRoleCache().
RemoveGroupRole(ctx context.Context, groupName, roleID string) error
// CreateRole creates a new role with permissions synced to Casbin.
//
// This is an out-of-band mutation operation that:
// 1. Validates label_scope_expr as valid go-bexpr syntax
// 2. Creates a Role record in the database
// 3. Parses actions (format "obj:act") and adds Casbin policies
// 4. Rolls back the database change if Casbin sync fails
//
// Parameters:
// - name: Role name (e.g., "platform-engineer")
// - description: Human-readable description
// - scopeExpr: Label scope expression (go-bexpr syntax, e.g., "env == 'prod'")
// - createConstraints: Map of label key → constraint (allowed values, required)
// - immutableKeys: List of label keys that cannot be changed
// - actions: List of actions in "obj:act" format (e.g., ["state:read", "state:write"])
//
// Returns the created role with generated ID, or error if validation/creation fails.
CreateRole(
ctx context.Context,
name, description, scopeExpr string,
createConstraints models.CreateConstraints,
immutableKeys []string,
actions []string,
) (*models.Role, error)
// UpdateRole updates an existing role's permissions and metadata.
//
// This is an out-of-band mutation operation that:
// 1. Validates label_scope_expr as valid go-bexpr syntax
// 2. Checks optimistic locking (version must match)
// 3. Updates the Role record in the database
// 4. Removes all old Casbin policies for the role
// 5. Adds new Casbin policies based on updated actions
//
// Parameters:
// - name: Role name (immutable, used for lookup)
// - expectedVersion: For optimistic locking (must match current version)
// - description, scopeExpr, createConstraints, immutableKeys, actions: Same as CreateRole
//
// Returns the updated role with incremented version, or error if validation/update fails.
// Returns error if version mismatch (concurrent modification detected).
UpdateRole(
ctx context.Context,
name string,
expectedVersion int,
description, scopeExpr string,
createConstraints models.CreateConstraints,
immutableKeys []string,
actions []string,
) (*models.Role, error)
// DeleteRole deletes a role and removes all associated Casbin policies.
//
// This is an out-of-band mutation operation that:
// 1. Verifies the role exists
// 2. Checks if the role is assigned to any principals (safety check)
// 3. Deletes the Role record from the database
// 4. Removes all Casbin policies for the role
//
// Safety: Rejects deletion if role is assigned to any principals.
// Returns error if role not found, still assigned, or deletion fails.
DeleteRole(ctx context.Context, name string) error
// GetRoleByName retrieves a role by its name.
// Returns repository.ErrNotFound if role doesn't exist.
GetRoleByName(ctx context.Context, name string) (*models.Role, error)
// GetRolesByName returns the roles matching the provided names along with metadata
// about invalid inputs and the complete set of valid role names.
//
// The returned roles preserve the order of roleNames and only include duplicates
// if the input slice does. Invalid names are surfaced so callers can craft
// contextual error messages without re-querying repositories.
GetRolesByName(ctx context.Context, roleNames []string) ([]models.Role, []string, []string, error)
// GetRoleByID retrieves a role by its internal ID.
// Returns repository.ErrNotFound if role doesn't exist.
GetRoleByID(ctx context.Context, roleID string) (*models.Role, error)
// ListAllRoles returns all roles in the system.
// Returns empty slice if no roles exist.
ListAllRoles(ctx context.Context) ([]models.Role, error)
// ListGroupRoles returns all group→role assignments, optionally filtered by group name.
// If groupName is nil, returns all assignments.
// If groupName is non-nil, returns only assignments for that group.
ListGroupRoles(ctx context.Context, groupName *string) ([]models.GroupRole, error)
// GetPrincipalRoles returns the Casbin role IDs for a principal.
// This replaces direct Enforcer.GetRolesForUser() calls in handlers.
//
// Parameters:
// - principalID: users.id or service_accounts.id (UUID)
// - principalType: "user" or "service_account"
//
// Returns: Array of Casbin role IDs (e.g., ["role::platform-engineer"]) with auth prefix.
GetPrincipalRoles(ctx context.Context, principalID, principalType string) ([]string, error)
// GetRolePermissions returns the Casbin permissions for a role.
// This replaces direct Enforcer.GetPermissionsForUser() calls in handlers.
//
// Parameters:
// - roleName: Role name (e.g., "platform-engineer")
//
// Returns: Array of permission tuples from Casbin (e.g., [["role::platform-engineer", "state", "read", ...]]).
GetRolePermissions(ctx context.Context, roleName string) ([][]string, error)
}
Service provides all identity and access management operations.
This service centralizes:
- Authentication (request path - performance critical)
- Authorization (request path - read-only Casbin)
- Session management (login/logout)
- User/service account management (admin operations)
- Role assignment (admin operations - triggers cache refresh)
- Cache management (out-of-band refresh)
func NewIAMService ¶
func NewIAMService(deps IAMServiceDependencies, cfg IAMServiceConfig) (Service, error)
NewIAMService creates a new IAM service with all dependencies.
This constructor:
- Initializes the GroupRoleCache with initial load from database
- Returns error if initial cache load fails (server cannot start)
- Initializes authenticators (SessionAuthenticator + JWTAuthenticator)
The cache initialization is critical - the server must not start if the cache cannot be loaded, as role resolution would fail for all requests.
type SessionAuthenticator ¶
type SessionAuthenticator struct {
// contains filtered or unexported fields
}
SessionAuthenticator authenticates requests using session cookies.
Implementation follows Phase 3 specification:
- Extract "grid.session" cookie
- Return (nil, nil) if not present
- Hash cookie value
- Lookup session in DB
- Validate: not revoked, not expired
- Lookup user
- Validate: not disabled
- Extract groups from session.id_token (stored JWT)
- Call ResolveRoles()
- Construct Principal
- Return Principal
This authenticator is stateless and thread-safe.
func NewSessionAuthenticator ¶
func NewSessionAuthenticator( users repository.UserRepository, sessions repository.SessionRepository, iamService Service, ) *SessionAuthenticator
NewSessionAuthenticator creates a new session authenticator.
func (*SessionAuthenticator) Authenticate ¶
func (a *SessionAuthenticator) Authenticate(ctx context.Context, req AuthRequest) (*Principal, error)
Authenticate extracts and validates session cookies.
Returns:
- (nil, nil) if no session cookie present (no credentials for this authenticator)
- (nil, error) if authentication fails (invalid session, expired, revoked, etc.)
- (*Principal, nil) if authentication succeeds