auth

package
v0.0.0-...-e5d423c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 26, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package auth provides authentication and authorization utilities. This file implements API key authentication for agents.

SECURITY: Agent API Key Authentication

Agents authenticate using API keys instead of JWT tokens because:

  • Agents are not users (no username/password)
  • Agents are long-running services (no interactive login)
  • API keys are simpler and more suitable for service-to-service auth

API Key Format:

  • 64 hexadecimal characters (32 bytes of randomness)
  • Generated using crypto/rand
  • Example: "a1b2c3d4e5f6...789" (64 chars)

API Key Storage:

  • Plaintext key given to agent ONCE during deployment
  • Bcrypt hash stored in database (cost factor 12)
  • Hash never exposed in API responses

API Key Usage:

  • Agent sends key in X-Agent-API-Key header
  • API validates key against bcrypt hash in database
  • Updates api_key_last_used_at on successful auth

API Key Rotation:

  • Admin can generate new key via /api/v1/admin/agents/:id/rotate-key
  • Old key immediately invalidated
  • New key returned ONCE (must be saved by admin)

Package auth provides authentication and authorization mechanisms for StreamSpace. This file implements HTTP handlers for authentication endpoints including local, SAML, and password management operations.

AUTHENTICATION HANDLERS: - Local authentication (username/password) - SAML SSO authentication (enterprise identity providers) - Token refresh (JWT token renewal) - Password change (local users only) - Logout (session termination)

SUPPORTED AUTHENTICATION FLOWS:

1. Local Authentication (POST /auth/login):

  • User submits username and password
  • System verifies credentials against database
  • Returns JWT token for subsequent requests
  • Supports account status validation (active/disabled)

2. SAML SSO Authentication (GET /auth/saml/login):

  • User initiates SSO flow
  • Redirects to enterprise IdP (Okta, Azure AD, etc.)
  • IdP sends SAML assertion after authentication
  • System validates assertion and creates local session
  • Returns JWT token for API access

3. Token Refresh (POST /auth/refresh):

  • Client submits existing JWT token
  • System validates token is within refresh window
  • Issues new token with extended expiration
  • Prevents indefinite token refresh (7-day window)

4. Password Change (POST /auth/password):

  • Local users can change their password
  • Requires current password verification
  • Not available for SSO users (SAML/OIDC)

SECURITY FEATURES:

- Password verification with bcrypt hashing - Account status validation (prevent disabled accounts from logging in) - JWT token generation with configurable expiration - SAML group synchronization for role-based access control - Secure cookie handling for SAML return URLs - Auto-provisioning of SSO users on first login

SAML GROUP SYNCHRONIZATION:

When users authenticate via SAML, their group memberships are automatically synchronized with StreamSpace groups: - SAML assertion contains groups claim from IdP - System matches SAML group names to local groups - Adds user to matching groups (does not remove from other groups by default) - Enables role-based access control based on IdP group membership

SECURITY CONSIDERATIONS:

1. Password Security:

  • Passwords hashed with bcrypt (cost factor 10+)
  • Never return password hashes in API responses
  • Minimum password length enforced (8 characters)

2. Account Lockout:

  • Disabled accounts cannot authenticate
  • Returns 403 Forbidden for disabled accounts
  • Prevents unauthorized access to suspended accounts

3. Token Security:

  • JWT tokens include user ID, role, and groups
  • Tokens expire after configured duration (default: 24 hours)
  • Refresh tokens only valid within 7-day window
  • See jwt.go for detailed token security

4. SAML Security:

  • Assertion signatures validated by middleware
  • Return URLs stored in secure cookies
  • SAML groups synced on every login
  • See saml.go for detailed SAML security

EXAMPLE USAGE:

// Initialize handler with dependencies
handler := NewAuthHandler(userDB, jwtManager, samlAuth)

// Register routes
router := gin.Default()
handler.RegisterRoutes(router.Group("/api/v1"))

// Routes will be available at:
// - POST /api/v1/auth/login (local authentication)
// - POST /api/v1/auth/refresh (token refresh)
// - POST /api/v1/auth/logout (logout)
// - GET  /api/v1/auth/saml/login (initiate SAML SSO)
// - POST /api/v1/auth/saml/acs (SAML callback)
// - GET  /api/v1/auth/saml/metadata (SAML SP metadata)

THREAD SAFETY:

All handler methods are thread-safe and can handle concurrent requests. Database operations use connection pooling for safe concurrent access.

Package auth provides authentication and authorization mechanisms for StreamSpace. This file implements JSON Web Token (JWT) authentication using HMAC-SHA256 signing.

JWT AUTHENTICATION OVERVIEW:

StreamSpace uses JWTs as the primary authentication mechanism for API requests. Tokens are issued after successful login and must be included in the Authorization header of subsequent requests.

TOKEN LIFECYCLE:

1. User logs in with username/password (or SSO) 2. System validates credentials 3. GenerateToken creates a signed JWT with user claims 4. Client stores token (typically in localStorage or httpOnly cookie) 5. Client includes token in Authorization header: "Bearer <token>" 6. Middleware validates token on each request 7. Token expires after configured duration (default: 24 hours) 8. User can refresh token within 7-day window before expiration 9. After expiration, user must re-authenticate

SECURITY FEATURES:

- HMAC-SHA256 signing prevents token tampering - Tokens include expiration time to limit exposure window - Refresh tokens only work within 7-day window (prevents infinite refresh) - Issuer claim prevents cross-site token reuse - NotBefore claim prevents premature token usage - Algorithm verification prevents algorithm substitution attacks

TOKEN STRUCTURE:

Header:

{
  "alg": "HS256",       // HMAC-SHA256 signing algorithm
  "typ": "JWT"          // Token type
}

Payload (Claims):

{
  "user_id": "user123",      // Internal user ID
  "username": "john.doe",    // Username for display
  "email": "john@example.com", // Email address
  "role": "user",            // Role: "admin", "operator", or "user"
  "groups": ["team-a"],      // Group memberships
  "iss": "streamspace-api",  // Issuer (prevents cross-site reuse)
  "sub": "user123",          // Subject (same as user_id)
  "iat": 1700000000,         // Issued at timestamp
  "exp": 1700086400,         // Expiration timestamp
  "nbf": 1700000000          // Not before timestamp
}

Signature:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)

SECURITY BEST PRACTICES:

1. Secret Key Management:

  • NEVER hardcode secret keys in source code
  • Load from environment variables or secret management systems
  • Use cryptographically random keys (at least 256 bits)
  • Rotate keys periodically (requires token invalidation strategy)

2. Token Storage (Client-Side):

  • Prefer httpOnly cookies over localStorage (prevents XSS attacks)
  • Use SameSite=Strict cookie attribute (prevents CSRF)
  • Set Secure flag for HTTPS-only transmission
  • Consider short-lived tokens with refresh token strategy

3. Token Validation:

  • Always verify signature before trusting claims
  • Check expiration time (exp claim)
  • Verify issuer matches expected value (iss claim)
  • Validate algorithm to prevent algorithm substitution attacks
  • Consider implementing token revocation list for compromised tokens

4. Attack Prevention:

  • Algorithm substitution: Verify signing method is HMAC (not "none")
  • Token replay: Use short expiration times and refresh mechanism
  • XSS: Store tokens in httpOnly cookies, not localStorage
  • CSRF: Include CSRF tokens or use SameSite cookies
  • Token theft: Use HTTPS only, short-lived tokens, rotation

COMMON VULNERABILITIES TO AVOID:

❌ Accepting tokens with "alg": "none" (no signature) ❌ Not validating token expiration ❌ Using weak secret keys (< 256 bits) ❌ Storing sensitive data in token payload (it's base64, not encrypted!) ❌ Not using HTTPS (tokens can be intercepted) ❌ Infinite token refresh (allows stolen tokens to live forever)

For more details on JWT security, see: - https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/ - https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens

Package auth provides authentication and authorization mechanisms for StreamSpace. This file implements Gin middleware for JWT token validation and role-based access control.

MIDDLEWARE COMPONENTS: - JWT authentication middleware (required authentication) - Optional authentication middleware (authentication not required) - Role-based authorization middleware (require specific roles) - Helper functions for extracting user context

AUTHENTICATION FLOW:

1. Client Request:

  • Client includes JWT token in Authorization header
  • Format: "Authorization: Bearer <token>"
  • Example: "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

2. Token Extraction:

  • Middleware extracts token from Authorization header
  • Validates header format (must start with "Bearer ")
  • Rejects requests with missing or malformed headers

3. Token Validation:

  • Validates JWT signature using secret key
  • Checks token expiration (exp claim)
  • Verifies token issuer (iss claim)
  • Ensures algorithm is HMAC (prevents algorithm substitution)

4. User Validation:

  • Extracts user ID from validated token claims
  • Queries database to verify user still exists
  • Checks if user account is active (not disabled)
  • Rejects requests from disabled or deleted users

5. Context Population:

  • Stores user information in Gin context
  • Available to downstream handlers via c.Get()
  • Includes: userID, username, email, role, groups

MIDDLEWARE TYPES:

1. Middleware (Required Authentication):

  • Rejects requests without valid JWT token
  • Returns 401 Unauthorized for invalid/missing tokens
  • Returns 403 Forbidden for disabled accounts
  • Use for protected API endpoints

2. OptionalAuth (Optional Authentication):

  • Accepts requests with or without token
  • Validates token if present, ignores if absent
  • Useful for endpoints that behave differently for authenticated users
  • Example: Public catalog with favorites for logged-in users

3. RequireRole (Role-Based Authorization):

  • Requires specific role (admin, operator, user)
  • Must be used after Middleware (requires authentication)
  • Returns 403 Forbidden if user lacks required role

4. RequireAnyRole (Multi-Role Authorization):

  • Accepts any of multiple roles
  • Example: RequireAnyRole("admin", "operator") for management endpoints
  • Returns 403 if user has none of the allowed roles

SECURITY FEATURES:

- Token signature validation prevents tampering - Expiration checking limits token lifetime - Active user validation prevents disabled account access - Role checking enforces principle of least privilege - Context isolation prevents request cross-contamination

SECURITY CONSIDERATIONS:

1. Token Transmission:

  • Tokens must be sent over HTTPS in production
  • Never log or expose tokens in error messages
  • Clear tokens from memory after use

2. Account Status:

  • Always check user.Active before allowing access
  • Disabled accounts cannot authenticate even with valid token
  • Supports immediate access revocation

3. Token Expiration:

  • Tokens expire after configured duration (default: 24 hours)
  • Expired tokens rejected with 401 Unauthorized
  • Forces periodic re-authentication

4. Role Validation:

  • Roles stored in JWT claims (tamper-proof via signature)
  • Role hierarchy: admin > operator > user
  • Always validate role before privileged operations

EXAMPLE USAGE:

// Require authentication for all /api routes
api := router.Group("/api")
api.Use(auth.Middleware(jwtManager, userDB))
{
    api.GET("/sessions", listSessions)  // Requires valid token
}

// Admin-only endpoints
admin := api.Group("/admin")
admin.Use(auth.RequireRole("admin"))
{
    admin.GET("/users", listAllUsers)  // Requires admin role
}

// Optional authentication (public + user features)
router.GET("/catalog", auth.OptionalAuth(jwtManager, userDB), showCatalog)

// Extract user info in handler
func listSessions(c *gin.Context) {
    userID, _ := auth.GetUserID(c)
    role, _ := auth.GetUserRole(c)
    // ... use user info
}

CONTEXT KEYS:

The middleware stores the following keys in Gin context: - "userID": string - Unique user identifier - "username": string - Username for display - "userEmail": string - User's email address - "userRole": string - Role (admin, operator, user) - "userGroups": []string - Group memberships - "claims": *Claims - Full JWT claims object

THREAD SAFETY:

All middleware functions are thread-safe and can handle concurrent requests. Each request gets its own Gin context, preventing data leakage between requests.

Package auth provides authentication and authorization mechanisms for StreamSpace. This file implements OpenID Connect (OIDC) authentication for integration with modern identity providers supporting OAuth 2.0 and OIDC standards.

OIDC AUTHENTICATION OVERVIEW:

OpenID Connect is an identity layer built on top of OAuth 2.0 that provides: - User authentication (not just authorization like OAuth 2.0) - Standard claims for user identity (sub, email, name, etc.) - ID tokens (JWT) containing user information - UserInfo endpoint for additional user details - Discovery mechanism for automatic configuration

SUPPORTED IDENTITY PROVIDERS:

- Keycloak (open source identity provider) - Okta (enterprise SSO platform) - Auth0 (identity as a service) - Google Workspace (Google accounts) - Azure AD / Microsoft Entra ID - GitHub (limited OIDC support) - GitLab (self-hosted or cloud) - Generic OIDC providers

OIDC AUTHENTICATION FLOW (Authorization Code Flow):

1. User Initiates Login:

  • User clicks "Login with [Provider]"
  • App redirects to /auth/oidc/login

2. Authorization Request:

3. User Authentication:

  • User authenticates at IdP (username/password, MFA, etc.)
  • IdP shows consent screen (if first time)
  • User approves requested scopes (openid, profile, email)

4. Authorization Code:

5. Token Exchange:

  • App exchanges authorization code for tokens
  • POST to IdP's token endpoint with code and client_secret
  • IdP returns: access_token, id_token, refresh_token (optional)

6. ID Token Validation:

  • App validates ID token signature using IdP's public key
  • App verifies claims: issuer, audience, expiration
  • App extracts user info from ID token claims

7. UserInfo Request (Optional):

  • App calls IdP's UserInfo endpoint with access token
  • Retrieves additional user attributes not in ID token
  • Merges with ID token claims

8. User Provisioning:

  • App creates or updates user in local database
  • Syncs user attributes from OIDC claims
  • Syncs group memberships if provided

9. Session Creation:

  • App generates JWT token for StreamSpace API
  • User is authenticated and can access protected resources

SECURITY FEATURES:

- State parameter validation (CSRF protection) - ID token signature validation (prevents tampering) - Nonce validation (prevents replay attacks) - Token expiration checking - TLS certificate validation for IdP connections - Client secret protection (never exposed to browser)

CONFIGURATION EXAMPLE:

config := &OIDCConfig{
    Enabled:      true,
    ProviderURL:  "https://accounts.google.com",  // Discovery URL
    ClientID:     "123456.apps.googleusercontent.com",
    ClientSecret: "your-client-secret",
    RedirectURI:  "https://streamspace.example.com/auth/oidc/callback",
    Scopes:       []string{"openid", "profile", "email", "groups"},
    UsernameClaim: "preferred_username",
    EmailClaim:    "email",
    GroupsClaim:   "groups",
}

SECURITY BEST PRACTICES:

1. Discovery URL:

  • Use HTTPS for provider URL
  • Validate TLS certificates (don't skip verification in production)
  • Provider URL should end at issuer root (not /...well-known/...)

2. Client Secret:

  • Never commit to version control
  • Load from environment variables or secret manager
  • Rotate periodically
  • Use separate secrets for dev/staging/production

3. Redirect URI:

  • Must exactly match URI registered with IdP
  • Use HTTPS in production (HTTP only for localhost dev)
  • Validate redirect URI to prevent open redirect attacks

4. State Parameter:

  • Generate cryptographically random state for each request
  • Store in cookie or session for validation
  • Prevents CSRF attacks

5. Token Validation:

  • Always validate ID token signature
  • Check expiration (exp claim)
  • Verify audience matches client_id (aud claim)
  • Verify issuer matches provider (iss claim)

COMMON OIDC VULNERABILITIES TO AVOID:

1. Missing State Validation:

  • Attack: Attacker initiates flow, tricks victim to complete
  • Prevention: Always validate state parameter matches

2. ID Token Signature Not Verified:

  • Attack: Attacker creates fake ID token with elevated privileges
  • Prevention: Always verify signature using IdP's public key

3. Open Redirect:

  • Attack: Attacker uses redirect_uri to redirect to malicious site
  • Prevention: Whitelist allowed redirect URIs

4. Client Secret Exposure:

  • Attack: Secret leaked in client-side code or logs
  • Prevention: Never include secret in frontend, use environment variables

ATTRIBUTE MAPPING:

Different IdPs use different claim names for user attributes. The OIDCConfig allows mapping IdP-specific claims to StreamSpace fields:

Keycloak:

UsernameClaim: "preferred_username"
EmailClaim:    "email"
GroupsClaim:   "groups"

Google:

UsernameClaim: "email"
EmailClaim:    "email"
GroupsClaim:   "groups" (Google Workspace only)

Azure AD:

UsernameClaim: "preferred_username"
EmailClaim:    "email"
GroupsClaim:   "groups"

EXAMPLE USAGE:

// Initialize OIDC authenticator
oidcAuth, err := NewOIDCAuthenticator(config)
if err != nil {
    log.Fatal(err)
}

// Register routes
router.GET("/auth/oidc/login", oidcAuth.OIDCLoginHandler)
router.GET("/auth/oidc/callback", oidcAuth.OIDCCallbackHandler(userManager))

// User flow:
// 1. Visit /auth/oidc/login
// 2. Redirect to IdP
// 3. Authenticate at IdP
// 4. Redirect to /auth/oidc/callback with code
// 5. Receive JWT token and user info

THREAD SAFETY:

The OIDCAuthenticator is thread-safe and can handle concurrent authentication requests. Each request maintains its own state and session isolation.

Package auth provides authentication and authorization mechanisms for StreamSpace. This file implements identity provider configuration templates and certificate management utilities for SAML and OIDC authentication.

IDENTITY PROVIDER SUPPORT:

This module provides pre-configured templates for popular identity providers, making it easier to integrate StreamSpace with enterprise SSO systems. It supports both SAML 2.0 and OpenID Connect (OIDC) protocols.

SUPPORTED SAML PROVIDERS:

- Okta: Enterprise SSO platform with comprehensive SAML support - Azure AD / Microsoft Entra ID: Microsoft's cloud identity provider - Google Workspace: Google's enterprise suite with SSO - Auth0: Identity as a Service platform - Keycloak: Open source identity and access management - Authentik: Modern open source identity provider - Generic: Template for any SAML 2.0 compliant provider

SUPPORTED OIDC PROVIDERS:

- Keycloak: Open source with full OIDC support - Okta: Enterprise OIDC provider - Auth0: OIDC identity service - Google: Google accounts with OIDC - Azure AD: Microsoft's OIDC implementation - GitHub: Developer accounts (limited OIDC) - GitLab: Self-hosted or cloud OIDC - Generic: Any OIDC-compliant provider

PROVIDER CONFIGURATION TEMPLATES:

Each provider has a pre-configured template that includes: - Default attribute mappings (email, username, groups, etc.) - Metadata URL template (for SAML) - Discovery URL template (for OIDC) - Default scopes (for OIDC) - Claim names for user attributes

WHY PROVIDER TEMPLATES?

Different identity providers use different attribute names and URL patterns:

Example - Email attribute: - Okta: "email" - Azure AD: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" - Google: "email" - Authentik: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"

Provider templates eliminate manual configuration and reduce integration errors.

SAML ATTRIBUTE MAPPING:

SAML attributes map user information from IdP to StreamSpace fields:

Okta Mapping:

Email:     "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
Username:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
FirstName: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
LastName:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
Groups:    "groups"

Azure AD Mapping:

Email:     "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
Username:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
FirstName: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
LastName:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
Groups:    "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"

Keycloak Mapping:

Email:     "email"
Username:  "username"
FirstName: "firstName"
LastName:  "lastName"
Groups:    "groups"

OIDC CLAIM MAPPING:

OIDC providers use simpler claim names than SAML:

Standard OIDC Claims:

UsernameClaim: "preferred_username"
EmailClaim:    "email"
GroupsClaim:   "groups"

Provider-Specific Variations:

Google:  username = "email" (no preferred_username)
Auth0:   groups = "https://{domain}/claims/groups" (namespaced)
GitHub:  username = "login", groups = "orgs"

METADATA URL TEMPLATES:

SAML providers expose metadata at predictable URLs. Templates use placeholders that are replaced with actual values during configuration:

Okta:

Template: "https://{domain}/app/{app_id}/sso/saml/metadata"
Example:  "https://dev-12345.okta.com/app/abc123/sso/saml/metadata"

Azure AD:

Template: "https://login.microsoftonline.com/{tenant_id}/federationmetadata/2007-06/federationmetadata.xml"
Example:  "https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/federationmetadata/..."

Keycloak:

Template: "https://{domain}/auth/realms/{realm}/protocol/saml/descriptor"
Example:  "https://auth.example.com/auth/realms/master/protocol/saml/descriptor"

CERTIFICATE MANAGEMENT:

SAML requires X.509 certificates for signing and encryption. This module provides utilities for loading certificates and private keys from PEM files:

- LoadCertificate: Loads X.509 certificate from PEM file - LoadPrivateKey: Loads RSA private key from PEM file (PKCS1 or PKCS8)

SECURITY CONSIDERATIONS:

1. Certificate Storage:

  • Store private keys securely (file permissions 0600)
  • Never commit private keys to version control
  • Use secrets management systems (Vault, AWS Secrets Manager)
  • Rotate certificates regularly (annually recommended)

2. Attribute Mapping Validation:

  • Verify attribute mappings with IdP administrator
  • Test with real user accounts before production
  • Log missing attributes for debugging

3. Metadata URLs:

  • Always use HTTPS for metadata fetching
  • Validate TLS certificates
  • Consider caching metadata to reduce dependencies

4. Provider Trust:

  • Only configure trusted identity providers
  • Verify IdP metadata before accepting
  • Monitor IdP configuration changes

EXAMPLE USAGE:

// Get Okta SAML configuration template
config := GetProviderConfig(ProviderOkta)
fmt.Printf("Email attribute: %s\n", config.DefaultMapping.Email)
// Output: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress

// Get Keycloak OIDC configuration template
oidcConfig := GetOIDCProviderConfig(OIDCProviderKeycloak)
fmt.Printf("Discovery URL: %s\n", oidcConfig.DiscoveryURL)
// Output: https://{domain}/auth/realms/{realm}

// Load SAML certificate
cert, err := LoadCertificate("/path/to/cert.pem")
if err != nil {
    log.Fatal(err)
}

// Load SAML private key
key, err := LoadPrivateKey("/path/to/key.pem")
if err != nil {
    log.Fatal(err)
}

AUTHENTICATION MODE CONFIGURATION:

StreamSpace supports multiple authentication modes:

- AuthModeJWT: Local authentication only (username/password + JWT) - AuthModeSAML: SAML SSO only (enterprise identity provider) - AuthModeOIDC: OIDC authentication only (modern IdPs) - AuthModeHybrid: Both local and SAML (flexible deployment)

Mode selection depends on deployment requirements: - Small teams: AuthModeJWT (simpler setup) - Enterprise: AuthModeSAML or AuthModeOIDC (centralized identity) - Mixed: AuthModeHybrid (support both employee SSO and external users)

THREAD SAFETY:

All functions in this module are thread-safe and can be called concurrently. Provider configuration templates are read-only and immutable.

Package auth provides authentication implementations for StreamSpace.

SAML 2.0 AUTHENTICATION

This file implements SAML 2.0 (Security Assertion Markup Language) authentication, enabling Single Sign-On (SSO) with enterprise identity providers like: - Okta - Azure AD / Microsoft Entra ID - Google Workspace - OneLogin - Auth0 - Keycloak

SAML 2.0 AUTHENTICATION FLOW:

The SAML authentication process follows the Service Provider (SP) initiated flow:

  1. User visits StreamSpace (Service Provider)
  2. User clicks "Login with SSO"
  3. SP generates SAML AuthnRequest (authentication request)
  4. User's browser redirects to IdP with AuthnRequest
  5. User authenticates with IdP (username/password, MFA, etc.)
  6. IdP generates SAML Assertion (signed XML with user attributes)
  7. User's browser POSTs assertion to SP's Assertion Consumer Service (ACS)
  8. SP validates assertion signature and extracts user attributes
  9. SP creates local session and issues JWT token
  10. User is authenticated and can access StreamSpace

SAML SECURITY FEATURES:

1. XML Signature Validation:

  • All assertions must be digitally signed by the IdP
  • SP verifies signature using IdP's public certificate
  • Prevents tampering with user attributes or session data

2. TLS Transport Security:

  • All SAML exchanges occur over HTTPS
  • Prevents man-in-the-middle attacks
  • Protects assertions in transit

3. Assertion Time Validation:

  • NotBefore: Assertion not valid before this time
  • NotOnOrAfter: Assertion expires after this time
  • Prevents replay attacks with old assertions

4. Audience Restriction:

  • Assertion specifies intended audience (SP entity ID)
  • Prevents assertions from being used at wrong service

5. InResponseTo Validation (SP-initiated flow):

  • Links assertion to original AuthnRequest
  • Prevents unsolicited assertions (unless AllowIDPInitiated=true)

CONFIGURATION EXAMPLE:

config := &SAMLConfig{
    Enabled:              true,
    EntityID:             "https://streamspace.example.com",
    MetadataURL:          "https://idp.example.com/metadata",
    AssertionConsumerURL: "https://streamspace.example.com/saml/acs",
    SingleLogoutURL:      "https://streamspace.example.com/saml/slo",
    Certificate:          spCert,      // SP's X.509 certificate
    PrivateKey:           spKey,       // SP's RSA private key
    AllowIDPInitiated:    false,       // Require SP-initiated flow
    SignRequest:          true,        // Sign AuthnRequests
    ForceAuthn:           false,       // Don't require re-authentication
    AttributeMapping: AttributeMapping{
        Email:     "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
        Username:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
        FirstName: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
        LastName:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
        Groups:    "http://schemas.xmlsoap.org/claims/Group",
    },
}

SECURITY BEST PRACTICES:

1. Always validate assertion signatures 2. Use TLS for all SAML endpoints 3. Set short assertion validity periods (5-10 minutes) 4. Disable IdP-initiated flow (AllowIDPInitiated=false) unless required 5. Sign AuthnRequests (SignRequest=true) when IdP supports it 6. Regularly rotate SP certificates 7. Validate SAML responses before creating sessions 8. Log all SAML authentication events for audit

COMMON SAML VULNERABILITIES TO AVOID:

1. XML Signature Wrapping (XSW):

  • Attack: Manipulate XML structure to bypass signature validation
  • Prevention: Use robust XML parsing library (crewjam/saml)

2. XML External Entity (XXE) Injection:

  • Attack: Reference external entities in XML to read files
  • Prevention: Disable external entity resolution in XML parser

3. Replay Attacks:

  • Attack: Reuse old SAML assertions
  • Prevention: Validate NotOnOrAfter, track assertion IDs

4. Man-in-the-Middle:

  • Attack: Intercept SAML assertion
  • Prevention: Use TLS, validate certificate chains

SUPPORTED IDENTITY PROVIDERS:

- Okta: Full support with metadata URL - Azure AD: Full support, map attributes correctly - Google Workspace: Requires custom attribute mapping - OneLogin: Full support with metadata URL - Auth0: Full support via SAML addon - Keycloak: Open source IdP, full support

Package auth provides authentication and authorization mechanisms for StreamSpace. This file implements server-side session tracking using Redis.

SESSION TRACKING:

StreamSpace uses server-side session tracking to provide: - Session invalidation on logout - Force re-login on application restart - Ability to revoke all sessions for a user - Session audit trail

HOW IT WORKS:

1. Token Generation:

  • Each JWT gets a unique session ID (jti claim)
  • Session metadata stored in Redis: session:{jti}
  • TTL matches token expiration

2. Token Validation:

  • Middleware checks if session exists in Redis
  • Missing session = invalid token (expired, revoked, or from before restart)
  • Valid session = allow request

3. Logout:

  • Delete session from Redis
  • Token immediately becomes invalid

4. Application Restart:

  • Redis pattern delete clears all sessions
  • All users must re-login

SECURITY BENEFITS:

- True logout: Sessions can be immediately invalidated - Compromise response: Revoke all user sessions on suspected breach - Multi-device management: Users can see and revoke active sessions - Forced re-authentication: Restart clears all sessions

Package auth provides authentication and authorization mechanisms for StreamSpace. This file implements secure token generation and hashing for API tokens, session tokens, and other authentication credentials.

TOKEN TYPES AND USE CASES:

StreamSpace uses different token types for different purposes:

1. API Tokens (Long-lived):

  • Used for programmatic API access
  • Stored in database with bcrypt hash
  • 384 bits of entropy (48 bytes)
  • Never expire (until revoked by user)
  • Example: Personal access tokens, integration keys

2. Session Tokens (Short-lived):

  • Used for web session management
  • Stored in database with SHA256 hash
  • 256 bits of entropy (32 bytes)
  • Expire after inactivity or logout
  • Example: Browser session cookies

3. Generic Secure Tokens:

  • Used for password reset, email verification, etc.
  • Stored with bcrypt hash for security
  • Configurable length
  • Single-use tokens with expiration

HASHING ALGORITHMS:

Two hashing algorithms are provided based on use case requirements:

1. bcrypt (For API Tokens):

  • Intentionally slow (prevents brute force attacks)
  • Adaptive work factor (can increase over time)
  • Salt included automatically
  • Recommended for long-lived tokens
  • Cost factor: 10 (good security/performance balance)

2. SHA256 (For Session Tokens):

  • Fast hashing (suitable for high-volume lookups)
  • 256-bit output (sufficient for session tokens)
  • No built-in salt (tokens are cryptographically random)
  • Recommended for short-lived, high-frequency tokens

WHY DIFFERENT ALGORITHMS?

bcrypt vs SHA256 trade-offs:

bcrypt Advantages: - Slow hashing prevents brute force attacks - Adaptive cost factor (security improves over time) - Best practice for password-like credentials

bcrypt Disadvantages: - Slower performance (intentional, but impacts throughput) - Not suitable for high-frequency validation

SHA256 Advantages: - Fast validation (thousands of lookups per second) - Suitable for session tokens with high request rates - Simple implementation

SHA256 Disadvantages: - Fast hashing makes brute force easier - No adaptive cost factor - Not recommended for long-lived credentials

SECURITY BEST PRACTICES:

1. Token Generation:

  • Use crypto/rand for cryptographically secure randomness
  • Never use math/rand (predictable, insecure)
  • Sufficient entropy: 32+ bytes for session, 48+ bytes for API
  • Base64 URL encoding for safe transmission

2. Token Storage:

  • NEVER store plain tokens in database
  • Always hash before storage (bcrypt or SHA256)
  • Store hash only, discard plain token after giving to user
  • Use prepared statements to prevent SQL injection

3. Token Transmission:

  • Send tokens over HTTPS only
  • Use secure, httpOnly cookies for session tokens
  • Never log or expose tokens in error messages
  • Clear tokens from memory after use

4. Token Expiration:

  • Session tokens: Short lifetime (hours to days)
  • API tokens: Long lifetime but allow user revocation
  • Reset tokens: Very short lifetime (minutes to hours)
  • Always enforce expiration checks

5. Token Revocation:

  • Support manual revocation by users
  • Revoke all tokens on password change
  • Track last used timestamp for auditing
  • Remove expired tokens regularly

TOKEN GENERATION PROCESS:

1. Generate Random Bytes:

  • Use crypto/rand.Read() for secure randomness
  • Generate sufficient bytes for desired entropy
  • Check for errors (rare, but possible on some systems)

2. Encode Plain Token:

  • Base64 URL encode random bytes
  • URL-safe encoding (no +, /, or = padding issues)
  • Result is alphanumeric string safe for URLs

3. Hash Token:

  • Hash plain token with bcrypt or SHA256
  • Store hash in database
  • Return plain token to user (shown only once)

4. User Storage:

  • User stores plain token securely (password manager, env var)
  • User includes token in API requests
  • Server hashes incoming token and compares with stored hash

EXAMPLE USAGE:

hasher := NewTokenHasher()

// Generate API token (long-lived, bcrypt)
plainToken, hashedToken, err := hasher.GenerateAPIToken()
if err != nil {
    log.Fatal(err)
}
// Store hashedToken in database
// Give plainToken to user (show only once!)

// Generate session token (short-lived, SHA256)
plainSession, hashedSession, err := hasher.GenerateSessionToken()
if err != nil {
    log.Fatal(err)
}
// Store hashedSession in database
// Set plainSession as secure cookie

// Verify API token (bcrypt)
valid := hasher.VerifyToken(userProvidedToken, storedHash)
if !valid {
    return errors.New("invalid token")
}

// Verify session token (SHA256 - faster)
valid := hasher.VerifyTokenSHA256(cookieToken, storedSessionHash)
if !valid {
    return errors.New("invalid session")
}

COMMON VULNERABILITIES TO AVOID:

1. Weak Random Number Generation:

  • ❌ DON'T use math/rand (predictable)
  • ✅ DO use crypto/rand (cryptographically secure)

2. Storing Plain Tokens:

  • ❌ DON'T store plain tokens in database
  • ✅ DO store only hashed tokens

3. Insufficient Entropy:

  • ❌ DON'T use short tokens (< 16 bytes)
  • ✅ DO use 32+ bytes for sessions, 48+ for API tokens

4. No Expiration:

  • ❌ DON'T allow tokens to live forever
  • ✅ DO enforce expiration and support revocation

5. Timing Attacks:

  • ❌ DON'T use == for token comparison
  • ✅ DO use bcrypt.CompareHashAndPassword (constant-time)

PERFORMANCE CONSIDERATIONS:

bcrypt Cost Factor Trade-offs:

Cost 10 (Default): - ~60ms per hash on modern CPU - ~16 hashes per second per core - Suitable for login and API token validation

Cost 12 (Higher Security): - ~240ms per hash - ~4 hashes per second per core - Use for extra-sensitive tokens

Cost 14 (Maximum Security): - ~960ms per hash - ~1 hash per second per core - May impact user experience

SHA256 Performance: - Microseconds per hash - Millions of hashes per second - Use for session tokens with high request rates

THREAD SAFETY:

All methods are thread-safe and can be called concurrently from multiple goroutines. Each token generation operation is independent.

Index

Constants

View Source
const (
	// APIKeyLength is the length of generated API keys in bytes (32 bytes = 64 hex chars)
	APIKeyLength = 32

	// BcryptCost is the cost factor for bcrypt hashing (12 = ~250ms per hash)
	BcryptCost = 12
)

Variables

This section is empty.

Functions

func CompareAPIKey

func CompareAPIKey(key, hash string) bool

CompareAPIKey compares a plaintext API key against a bcrypt hash.

Returns true if the key matches the hash, false otherwise.

Example:

valid := CompareAPIKey("a1b2c3d4e5f6...789", storedHash)
if valid {
    // Key is valid
}

func GenerateAPIKey

func GenerateAPIKey() (string, error)

GenerateAPIKey generates a cryptographically random API key.

Returns a 64-character hexadecimal string (32 bytes of randomness).

Example:

key, err := GenerateAPIKey()
// key = "a1b2c3d4e5f6...789" (64 chars)

func GenerateSessionID

func GenerateSessionID() (string, error)

GenerateSessionID creates a cryptographically random session ID

func GetUserID

func GetUserID(c *gin.Context) (string, bool)

GetUserID extracts the user ID from the Gin context

func GetUserRole

func GetUserRole(c *gin.Context) (string, bool)

GetUserRole extracts the user role from the Gin context

func GetUsername

func GetUsername(c *gin.Context) (string, bool)

GetUsername extracts the username from the Gin context

func HashAPIKey

func HashAPIKey(key string) (string, error)

HashAPIKey hashes an API key using bcrypt.

The hash can be safely stored in the database and compared against provided keys using CompareAPIKey.

Cost factor is set to 12 (~250ms per hash) for security.

Example:

hash, err := HashAPIKey("a1b2c3d4e5f6...789")
// Store hash in database

func IsAdmin

func IsAdmin(c *gin.Context) bool

IsAdmin checks if the current user is an admin

func IsOperator

func IsOperator(c *gin.Context) bool

IsOperator checks if the current user is an operator or admin

func LoadCertificate

func LoadCertificate(certPath string) (*x509.Certificate, error)

LoadCertificate loads an X.509 certificate from PEM file

func LoadPrivateKey

func LoadPrivateKey(keyPath string) (*rsa.PrivateKey, error)

LoadPrivateKey loads an RSA private key from PEM file

func Middleware

func Middleware(jwtManager *JWTManager, userDB *db.UserDB) gin.HandlerFunc

Middleware creates an authentication middleware that validates JWT tokens and ensures user accounts are active.

WEBSOCKET HANDLING: WebSocket upgrade requests receive special treatment to maintain protocol compatibility: - Detected by checking Upgrade=websocket and Connection=Upgrade headers - On auth failure: Returns status code only (no JSON body) via AbortWithStatus - Rationale: WebSocket upgrader expects clean HTTP responses without body content - Standard requests: Returns JSON error messages as usual

This dual-response approach was added to fix WebSocket connection issues where JSON error responses would interfere with the WebSocket handshake protocol.

func OptionalAuth

func OptionalAuth(jwtManager *JWTManager, userDB *db.UserDB) gin.HandlerFunc

OptionalAuth middleware allows both authenticated and unauthenticated requests

func RequireAnyRole

func RequireAnyRole(roles ...string) gin.HandlerFunc

RequireAnyRole middleware requires one of multiple roles

func RequireRole

func RequireRole(requiredRole string) gin.HandlerFunc

RequireRole middleware requires a specific role

func ValidateAPIKeyFormat

func ValidateAPIKeyFormat(key string) error

ValidateAPIKeyFormat checks if an API key has the correct format.

Valid format: 64 hexadecimal characters (32 bytes)

Returns error if format is invalid.

Example:

if err := ValidateAPIKeyFormat(key); err != nil {
    return fmt.Errorf("invalid API key format: %w", err)
}

func ValidateConfig

func ValidateConfig(config *AuthConfig) error

ValidateConfig validates the authentication configuration

Types

type APIKeyMetadata

type APIKeyMetadata struct {
	// PlaintextKey is the unhashed API key (64 hex chars)
	// SECURITY: This should only be shown to the admin ONCE
	PlaintextKey string

	// Hash is the bcrypt hash of the key
	// This is what gets stored in the database
	Hash string

	// CreatedAt is when the key was generated
	CreatedAt time.Time
}

APIKeyMetadata contains metadata about an API key.

Used when generating new keys to return both the plaintext key and metadata for storage in the database.

func GenerateAPIKeyWithMetadata

func GenerateAPIKeyWithMetadata() (*APIKeyMetadata, error)

GenerateAPIKeyWithMetadata generates a new API key and returns both the plaintext key and metadata for database storage.

The plaintext key should be shown to the admin ONCE and then discarded. Only the hash should be stored in the database.

Example:

metadata, err := GenerateAPIKeyWithMetadata()
if err != nil {
    return err
}

// Show to admin ONCE
fmt.Printf("New API key: %s\n", metadata.PlaintextKey)
fmt.Println("SAVE THIS KEY - it will not be shown again")

// Store in database
_, err = db.Exec(
    "UPDATE agents SET api_key_hash = $1, api_key_created_at = $2 WHERE id = $3",
    metadata.Hash, metadata.CreatedAt, agentID,
)

type AttributeMapping

type AttributeMapping struct {
	Email     string // SAML attribute name for email
	Username  string // SAML attribute name for username
	FirstName string // SAML attribute name for first name
	LastName  string // SAML attribute name for last name
	Groups    string // SAML attribute name for groups
}

AttributeMapping maps SAML attributes to user fields

type AuthConfig

type AuthConfig struct {
	// JWT configuration (existing)
	JWTSecret     string
	JWTExpiration int

	// SAML configuration
	SAML *SAMLConfig

	// OIDC configuration
	OIDC *OIDCConfig

	// Authentication mode
	Mode AuthMode
}

AuthConfig represents the complete authentication configuration

type AuthHandler

type AuthHandler struct {
	// contains filtered or unexported fields
}

AuthHandler handles authentication requests

func NewAuthHandler

func NewAuthHandler(userDB UserStore, jwtManager TokenManager, samlAuth SAMLService) *AuthHandler

NewAuthHandler creates a new auth handler

func (*AuthHandler) ChangePassword

func (h *AuthHandler) ChangePassword(c *gin.Context)

ChangePassword handles password changes for local users

func (*AuthHandler) Login

func (h *AuthHandler) Login(c *gin.Context)

Login handles user login

func (*AuthHandler) Logout

func (h *AuthHandler) Logout(c *gin.Context)

Logout handles logout and invalidates the session in Redis

func (*AuthHandler) RefreshToken

func (h *AuthHandler) RefreshToken(c *gin.Context)

RefreshToken handles token refresh

func (*AuthHandler) RegisterRoutes

func (h *AuthHandler) RegisterRoutes(router *gin.RouterGroup)

RegisterRoutes registers authentication routes

func (*AuthHandler) SAMLCallback

func (h *AuthHandler) SAMLCallback(c *gin.Context)

SAMLCallback handles SAML assertion callback

func (*AuthHandler) SAMLLogin

func (h *AuthHandler) SAMLLogin(c *gin.Context)

SAMLLogin initiates SAML authentication flow

func (*AuthHandler) SAMLMetadata

func (h *AuthHandler) SAMLMetadata(c *gin.Context)

SAMLMetadata returns SAML service provider metadata

type AuthMode

type AuthMode string

AuthMode defines the authentication mode

const (
	AuthModeJWT    AuthMode = "jwt"    // JWT only (default)
	AuthModeSAML   AuthMode = "saml"   // SAML only
	AuthModeHybrid AuthMode = "hybrid" // Both JWT and SAML
	AuthModeOIDC   AuthMode = "oidc"   // OIDC authentication
)

type Claims

type Claims struct {
	// UserID is the unique internal identifier for the user.
	// Used to look up user details in the database.
	// Also set in the standard "sub" (subject) claim.
	UserID string `json:"user_id"`

	// OrgID is the organization this user belongs to.
	// SECURITY CRITICAL: This field enables multi-tenancy isolation.
	// All API handlers MUST filter queries by org_id to prevent
	// cross-tenant data access.
	OrgID string `json:"org_id"`

	// OrgName is the human-readable organization name.
	// Used for display purposes only.
	OrgName string `json:"org_name,omitempty"`

	// K8sNamespace is the Kubernetes namespace for this org's resources.
	// Used by WebSocket handlers to scope session/metrics queries.
	K8sNamespace string `json:"k8s_namespace,omitempty"`

	// Username is the user's login name.
	// Used for display purposes and audit logs.
	Username string `json:"username"`

	// Email is the user's email address.
	// Used for notifications and account recovery.
	Email string `json:"email"`

	// Role defines the user's system-wide permission level.
	// Values: "admin", "operator", "user"
	// - admin: Full system access (all APIs, all users)
	// - operator: Platform management (view all, manage resources)
	// - user: Standard access (own sessions only)
	Role string `json:"role"`

	// OrgRole defines the user's role within their organization.
	// Values: "org_admin", "maintainer", "user", "viewer"
	// - org_admin: Manage users/roles, templates, org settings
	// - maintainer: Manage templates, sessions (no user admin)
	// - user: Manage own sessions, list org templates
	// - viewer: Read-only access to lists/metrics
	OrgRole string `json:"org_role,omitempty"`

	// Groups lists the teams/groups the user belongs to.
	// Used for team-based resource sharing and quotas.
	// Omitted from token if user has no group memberships.
	Groups []string `json:"groups,omitempty"`

	// RegisteredClaims contains standard JWT claims:
	// - iss (issuer): Who created the token
	// - sub (subject): User ID (same as UserID above)
	// - iat (issued at): When token was created
	// - exp (expiration): When token expires
	// - nbf (not before): When token becomes valid
	jwt.RegisteredClaims
}

Claims represents custom JWT claims for StreamSpace users.

This struct extends the standard JWT claims with StreamSpace-specific user information. All fields are included in the token payload (which is base64-encoded, NOT encrypted).

SECURITY WARNING: Do not include sensitive information in claims! - ❌ DON'T include passwords, API keys, credit card numbers - ❌ DON'T include SSNs, health data, or other PII beyond what's necessary - ✅ DO include user IDs, org IDs, roles, and group memberships - ✅ DO keep claim data minimal to reduce token size

MULTI-TENANCY: The OrgID field is CRITICAL for tenant isolation. All API handlers MUST extract org_id from claims and use it to filter database queries. Never trust client-provided org_id values.

Token payload is visible to anyone with the token (it's only base64-encoded). Only the signature prevents tampering, not visibility.

type JWTConfig

type JWTConfig struct {
	// SecretKey is the HMAC signing key for tokens.
	// SECURITY: Must be cryptographically random (use crypto/rand).
	// Minimum length: 32 bytes (256 bits) for HS256.
	// Example generation: openssl rand -base64 32
	SecretKey string

	// Issuer identifies who issued the token (typically your API name).
	// Used to prevent tokens from one system being used on another.
	// Default: "streamspace-api"
	Issuer string

	// TokenDuration is how long tokens remain valid.
	// Balance security (shorter is better) with user experience.
	// Recommended: 1-24 hours for web apps, 15-60 minutes for APIs.
	// Default: 24 hours
	TokenDuration time.Duration
}

JWTConfig holds JWT configuration.

SECURITY: SecretKey must be cryptographically random and at least 256 bits. Never hardcode this value - load from environment variables or secrets management.

Example configuration:

config := &JWTConfig{
    SecretKey:     os.Getenv("JWT_SECRET_KEY"),  // From environment
    Issuer:        "streamspace-api",             // Your API identifier
    TokenDuration: 24 * time.Hour,                // 24-hour token lifetime
}

type JWTManager

type JWTManager struct {
	// contains filtered or unexported fields
}

JWTManager handles JWT token operations

func NewJWTManager

func NewJWTManager(config *JWTConfig) *JWTManager

NewJWTManager creates a new JWT manager

func NewJWTManagerWithSessions

func NewJWTManagerWithSessions(config *JWTConfig, cacheClient *cache.Cache) *JWTManager

NewJWTManagerWithSessions creates a new JWT manager with session tracking

func (*JWTManager) ClearAllSessions

func (m *JWTManager) ClearAllSessions(ctx context.Context) error

ClearAllSessions clears all sessions (force re-login on restart)

func (*JWTManager) ExtractUserID

func (m *JWTManager) ExtractUserID(tokenString string) (string, error)

ExtractUserID extracts the user ID from a token without full validation

func (*JWTManager) GenerateToken

func (m *JWTManager) GenerateToken(userID, username, email, role string, groups []string) (string, error)

GenerateToken generates a new JWT token for a user.

This function creates a cryptographically signed JWT token containing user identity and permission information. The token is signed using HMAC-SHA256 to prevent tampering.

TOKEN GENERATION PROCESS:

1. Create Claims:

  • User identity: UserID, Username, Email
  • Organization: OrgID, OrgName, K8sNamespace (CRITICAL for multi-tenancy)
  • Permissions: Role (admin/operator/user), OrgRole, Groups
  • Standard claims: Issuer, Subject, IssuedAt, ExpiresAt, NotBefore

2. Create Token:

  • Header: {"alg": "HS256", "typ": "JWT"}
  • Payload: Base64URL(claims JSON)
  • Signature: HMACSHA256(header + payload, secret_key)

3. Return Token:

  • Format: "header.payload.signature" (base64url-encoded)
  • Example: "eyJhbGc...header.eyJ1c2VyX2lk...payload.SflKxwRJ...signature"

SECURITY CONSIDERATIONS:

- Uses HS256 (HMAC-SHA256) signing algorithm

  • Symmetric key (same key for signing and verification)
  • 256-bit security strength
  • Fast and secure for server-to-server authentication

- Includes expiration time (exp claim)

  • Tokens automatically become invalid after TokenDuration
  • Default: 24 hours
  • Limits damage from stolen tokens

- Includes "not before" time (nbf claim)

  • Token cannot be used before creation time
  • Prevents premature token usage

- Includes issuer (iss claim)

  • Identifies the token creator
  • Prevents tokens from other systems being accepted

MULTI-TENANCY:

- Includes org_id in claims (CRITICAL for tenant isolation) - All API handlers MUST extract org_id and use it to filter queries - Never trust client-provided org_id values

USAGE EXAMPLE:

manager := NewJWTManager(&JWTConfig{
    SecretKey:     "your-secret-key-min-32-bytes",
    Issuer:        "streamspace-api",
    TokenDuration: 24 * time.Hour,
})

token, err := manager.GenerateToken(
    "user123",           // userID
    "john.doe",          // username
    "john@example.com",  // email
    "user",              // role
    []string{"team-a"},  // groups
)
if err != nil {
    log.Fatal(err)
}

// Token can now be sent to client
// Client includes in requests: Authorization: Bearer <token>

PARAMETERS:

  • userID: Unique user identifier (required, non-empty)
  • username: Display name for user (required, non-empty)
  • email: User's email address (required for notifications)
  • role: Permission level - "admin", "operator", or "user"
  • groups: Team/group memberships (can be empty array or nil)

RETURNS:

  • string: Signed JWT token ready for transmission to client
  • error: If token signing fails (should never happen with valid config)

COMMON ERRORS:

  • "failed to sign token": SecretKey is invalid or empty

NOTE: The generated token contains sensitive information (user identity, role). Always transmit tokens over HTTPS to prevent interception.

DEPRECATED: Use GenerateTokenWithOrg for multi-tenant deployments.

func (*JWTManager) GenerateTokenWithContext

func (m *JWTManager) GenerateTokenWithContext(ctx context.Context, userID, username, email, role string, groups []string, ipAddress, userAgent string) (string, error)

GenerateTokenWithContext generates a new JWT token with session tracking. DEPRECATED: Use GenerateTokenWithOrg for multi-tenant deployments. This function is kept for backward compatibility and defaults to "default-org".

func (*JWTManager) GenerateTokenWithOrg

func (m *JWTManager) GenerateTokenWithOrg(ctx context.Context, userID, username, email, role string, groups []string, orgInfo *OrgInfo, ipAddress, userAgent string) (string, error)

GenerateTokenWithOrg generates a JWT token with organization context.

This is the preferred method for multi-tenant deployments. It includes org_id in the token claims, which is CRITICAL for tenant isolation.

SECURITY: All API handlers MUST extract org_id from claims and use it to filter database queries. Never trust client-provided org_id values.

func (*JWTManager) GetSessionStore

func (m *JWTManager) GetSessionStore() *SessionStore

GetSessionStore returns the session store

func (*JWTManager) GetTokenDuration

func (m *JWTManager) GetTokenDuration() time.Duration

GetTokenDuration returns the configured token duration

func (*JWTManager) InvalidateSession

func (m *JWTManager) InvalidateSession(ctx context.Context, sessionID string) error

InvalidateSession invalidates a session by its ID (logout)

func (*JWTManager) InvalidateUserSessions

func (m *JWTManager) InvalidateUserSessions(ctx context.Context, userID string) error

InvalidateUserSessions invalidates all sessions for a user

func (*JWTManager) RefreshToken

func (m *JWTManager) RefreshToken(tokenString string) (string, error)

RefreshToken generates a new token with extended expiration.

This function allows users to get a new token without re-authenticating, but only within a specific time window before expiration. This balances security (limiting lifetime of compromised tokens) with user experience (avoiding frequent re-authentication).

REFRESH WINDOW LOGIC:

Tokens can only be refreshed when they have between 0 and 7 days remaining:

Token Age          | Remaining Time | Refresh Allowed?
-------------------|----------------|------------------
Fresh (< 17 days)  | > 7 days       | ❌ No (too early)
Middle (17-24 days)| 0-7 days       | ✅ Yes (refresh window)
Expired (> 24 days)| < 0 days       | ❌ No (expired)

WHY 7-DAY WINDOW?

1. Prevents Infinite Token Life:

  • Without a window, users could refresh tokens forever
  • A stolen token could be refreshed indefinitely by an attacker
  • 7-day window limits exposure: max token age = 24 days (17 + 7)

2. Balances Security vs UX:

  • Too short (e.g., 1 day): Frequent re-authentication annoys users
  • Too long (e.g., 30 days): Compromised tokens live too long
  • 7 days: Provides flexibility while limiting risk

3. Forces Periodic Re-Authentication:

  • Every 24 days (at most), users must provide credentials again
  • Ensures disabled accounts eventually lose access
  • Gives time to detect and respond to account compromises

TOKEN REFRESH FLOW:

Day 0: User logs in, gets token (expires Day 24) Day 10: User tries to refresh -> "too early" (14 days remaining) Day 18: User tries to refresh -> Success! (6 days remaining)

New token issued (expires Day 42)

Day 25: User tries to refresh old token -> "expired"

New token still valid until Day 42

Day 36: User tries to refresh -> Success! (6 days remaining)

New token issued (expires Day 60)

SECURITY CONSIDERATIONS:

1. Refresh Uses Validation:

  • Old token is fully validated before refresh (signature, expiration)
  • Cannot refresh invalid or tampered tokens
  • Cannot refresh tokens with wrong algorithm

2. Window Prevents Infinite Refresh:

  • Tokens > 7 days from expiration cannot be refreshed
  • Limits max token age even with continuous refresh
  • Forces re-authentication every ~24-30 days

3. Expired Tokens Rejected:

  • Cannot refresh tokens that have already expired
  • Expired tokens must go through full authentication

4. New Token Has Same Claims:

  • User ID, role, groups copied from old token
  • Cannot escalate privileges by refreshing
  • Only timestamps are updated (iat, exp, nbf)

ALTERNATIVE APPROACHES:

Other common refresh strategies (for comparison):

1. Separate Refresh Tokens:

  • Short-lived access tokens (15 min) + long-lived refresh tokens (30 days)
  • More complex: requires two token types
  • Better security: compromised access token expires quickly

2. Sliding Expiration:

  • Each API call extends token expiration
  • Simple implementation
  • Risk: Stolen tokens never expire if used regularly

3. No Refresh:

  • Tokens expire and user must re-authenticate
  • Maximum security
  • Poor UX: frequent logins annoy users

StreamSpace uses the 7-day window approach as a balance between these extremes.

USAGE EXAMPLE:

// In API endpoint /api/v1/auth/refresh
func handleRefresh(c *gin.Context) {
    oldToken := c.Request.Header.Get("Authorization")
    oldToken = strings.TrimPrefix(oldToken, "Bearer ")

    newToken, err := jwtManager.RefreshToken(oldToken)
    if err != nil {
        // Token expired, not in window, or invalid
        c.JSON(401, gin.H{"error": err.Error()})
        return
    }

    // Return new token to client
    c.JSON(200, gin.H{"token": newToken})
}

PARAMETERS:

  • tokenString: Current JWT token (must be valid and in refresh window)

RETURNS:

  • string: New token with extended expiration (new 24-hour lifetime)
  • error: If token is invalid, expired, or not in refresh window

COMMON ERRORS:

  • "token has already expired": Token exp claim has passed (must re-authenticate)
  • "token not eligible for refresh yet": Token has > 7 days remaining (too early)
  • "failed to parse token": Token is invalid, tampered, or wrong algorithm

REFRESH TIMING RECOMMENDATION:

Client should refresh tokens proactively: - Check token expiration on app startup - If < 7 days remaining, call /api/v1/auth/refresh - If refresh fails, redirect to login page - Consider refreshing daily to maintain continuous access

func (*JWTManager) SetSessionStore

func (m *JWTManager) SetSessionStore(store *SessionStore)

SetSessionStore sets the session store for server-side session tracking

func (*JWTManager) ValidateSession

func (m *JWTManager) ValidateSession(ctx context.Context, sessionID string) (bool, error)

ValidateSession checks if a session is valid (exists in Redis)

func (*JWTManager) ValidateToken

func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error)

ValidateToken validates a JWT token and returns the claims.

This function performs comprehensive validation of a JWT token, including: - Signature verification (prevents tampering) - Algorithm verification (prevents algorithm substitution attacks) - Expiration checking (ensures token hasn't expired) - Claim extraction (returns user information)

VALIDATION PROCESS:

1. Parse Token:

  • Split token into header, payload, signature
  • Base64URL-decode header and payload
  • Parse claims into Claims struct

2. Verify Algorithm:

  • SECURITY: Check that algorithm is HMAC (not "none" or asymmetric)
  • Prevent algorithm substitution attacks
  • Reject tokens using unexpected signing methods

3. Verify Signature:

  • Compute HMACSHA256(header + payload, secret_key)
  • Compare with signature in token
  • Reject if signatures don't match (token was tampered with)

4. Verify Expiration:

  • Check exp claim against current time
  • Reject if token has expired

5. Verify Not Before:

  • Check nbf claim against current time
  • Reject if token is being used too early

6. Return Claims:

  • Extract user information from validated token
  • Safe to trust claims after validation succeeds

SECURITY: ALGORITHM SUBSTITUTION ATTACK PREVENTION

This function explicitly verifies the signing method is HMAC before accepting the token. This prevents a critical vulnerability where attackers could:

1. Take a valid token signed with HS256 2. Change algorithm to "none" in header 3. Remove signature 4. Server accepts token without verification

Or asymmetric algorithm substitution:

1. Take a valid HS256 token 2. Change algorithm to RS256 (RSA public key) 3. Sign with HMAC using the public key (known to attacker) 4. Server treats public key as symmetric key and validates successfully

By verifying token.Method is *jwt.SigningMethodHMAC, we reject both attacks.

USAGE EXAMPLE:

// In middleware
authHeader := c.Request.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
    c.AbortWithStatus(401)
    return
}

tokenString := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := jwtManager.ValidateToken(tokenString)
if err != nil {
    c.JSON(401, gin.H{"error": "Invalid token"})
    return
}

// Token is valid - extract user info
userID := claims.UserID
role := claims.Role
c.Set("user_id", userID)
c.Set("role", role)
c.Next()

PARAMETERS:

  • tokenString: JWT token in format "header.payload.signature" Typically extracted from Authorization header: "Bearer <token>"

RETURNS:

  • *Claims: User information and metadata from validated token
  • error: Validation failure (tampered token, expired, wrong algorithm, etc.)

COMMON ERRORS:

  • "unexpected signing method": Token uses wrong algorithm (attack attempt)
  • "failed to parse token: token is expired": Token exp claim has passed
  • "invalid token": Token signature verification failed (tampered)
  • "token used before issued": nbf (not before) claim is in future

SECURITY NOTE: Never skip validation even for "trusted" tokens. Always validate signature, expiration, and algorithm.

type LoginRequest

type LoginRequest struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

LoginRequest represents a login request

type LoginResponse

type LoginResponse struct {
	Token     string       `json:"token"`
	ExpiresAt time.Time    `json:"expiresAt"`
	User      *models.User `json:"user"`
}

LoginResponse represents a login response

type OIDCAuthenticator

type OIDCAuthenticator struct {
	// contains filtered or unexported fields
}

OIDCAuthenticator handles OIDC authentication

func NewOIDCAuthenticator

func NewOIDCAuthenticator(config *OIDCConfig) (*OIDCAuthenticator, error)

NewOIDCAuthenticator creates a new OIDC authenticator

func (*OIDCAuthenticator) GetAuthorizationURL

func (a *OIDCAuthenticator) GetAuthorizationURL(state string) string

GetAuthorizationURL generates the OIDC authorization URL

func (*OIDCAuthenticator) GetDiscoveryDocument

func (a *OIDCAuthenticator) GetDiscoveryDocument() (map[string]interface{}, error)

GetDiscoveryDocument returns the OIDC discovery document

func (*OIDCAuthenticator) HandleCallback

func (a *OIDCAuthenticator) HandleCallback(ctx context.Context, code string) (*OIDCUserInfo, error)

HandleCallback processes the OIDC callback and returns user information

func (*OIDCAuthenticator) OIDCCallbackHandler

func (a *OIDCAuthenticator) OIDCCallbackHandler(userManager UserManager) gin.HandlerFunc

OIDCCallbackHandler handles the OIDC callback

func (*OIDCAuthenticator) OIDCLoginHandler

func (a *OIDCAuthenticator) OIDCLoginHandler(c *gin.Context)

OIDCLoginHandler initiates OIDC authentication flow

type OIDCConfig

type OIDCConfig struct {
	Enabled            bool              `json:"enabled"`
	ProviderURL        string            `json:"provider_url"`         // OIDC provider discovery URL
	ClientID           string            `json:"client_id"`            // OAuth2 client ID
	ClientSecret       string            `json:"client_secret"`        // OAuth2 client secret
	RedirectURI        string            `json:"redirect_uri"`         // OAuth2 redirect URI
	Scopes             []string          `json:"scopes"`               // OAuth2 scopes (default: openid, profile, email)
	UsernameClaim      string            `json:"username_claim"`       // Claim to use for username (default: preferred_username)
	EmailClaim         string            `json:"email_claim"`          // Claim to use for email (default: email)
	GroupsClaim        string            `json:"groups_claim"`         // Claim to use for groups (default: groups)
	RolesClaim         string            `json:"roles_claim"`          // Claim to use for roles (default: roles)
	ExtraParams        map[string]string `json:"extra_params"`         // Additional OAuth2 parameters
	InsecureSkipVerify bool              `json:"insecure_skip_verify"` // Skip TLS verification (dev only)
}

OIDCConfig holds OIDC authentication configuration

type OIDCProvider

type OIDCProvider string

OIDCProvider represents an OIDC identity provider configuration

const (
	// Supported OIDC providers
	OIDCProviderKeycloak OIDCProvider = "keycloak"
	OIDCProviderOkta     OIDCProvider = "okta"
	OIDCProviderAuth0    OIDCProvider = "auth0"
	OIDCProviderGoogle   OIDCProvider = "google"
	OIDCProviderAzureAD  OIDCProvider = "azuread"
	OIDCProviderGitHub   OIDCProvider = "github"
	OIDCProviderGitLab   OIDCProvider = "gitlab"
	OIDCProviderGeneric  OIDCProvider = "generic"
)

type OIDCProviderConfig

type OIDCProviderConfig struct {
	Provider      OIDCProvider
	DiscoveryURL  string   // Well-known discovery URL
	DefaultScopes []string // Default OAuth2 scopes
	UsernameClaim string   // Default username claim
	EmailClaim    string   // Default email claim
	GroupsClaim   string   // Default groups claim
}

OIDCProviderConfig holds OIDC provider-specific configuration templates

func GetOIDCProviderConfig

func GetOIDCProviderConfig(provider OIDCProvider) *OIDCProviderConfig

GetOIDCProviderConfig returns the configuration template for an OIDC provider

type OIDCUserInfo

type OIDCUserInfo struct {
	Subject       string                 `json:"sub"`
	Email         string                 `json:"email"`
	Username      string                 `json:"username"`
	EmailVerified bool                   `json:"email_verified"`
	FirstName     string                 `json:"given_name,omitempty"`
	LastName      string                 `json:"family_name,omitempty"`
	FullName      string                 `json:"name,omitempty"`
	Picture       string                 `json:"picture,omitempty"`
	Groups        []string               `json:"groups,omitempty"`
	Roles         []string               `json:"roles,omitempty"`
	Claims        map[string]interface{} `json:"claims,omitempty"`
}

OIDCUserInfo holds user information extracted from OIDC tokens

type OrgInfo

type OrgInfo struct {
	// OrgID is the organization's unique identifier.
	OrgID string

	// OrgName is the human-readable organization name.
	OrgName string

	// K8sNamespace is the Kubernetes namespace for this org.
	K8sNamespace string

	// OrgRole is the user's role within this organization.
	OrgRole string
}

OrgInfo contains organization information for token generation. This is used to include org context in JWT claims.

type PasswordChangeRequest

type PasswordChangeRequest struct {
	OldPassword string `json:"oldPassword" binding:"required"`
	NewPassword string `json:"newPassword" binding:"required,min=8"`
}

PasswordChangeRequest represents a password change request

type ProviderConfig

type ProviderConfig struct {
	Provider            SAMLProvider
	DefaultMapping      AttributeMapping
	MetadataURLTemplate string
}

ProviderConfig holds provider-specific configuration templates

func GetProviderConfig

func GetProviderConfig(provider SAMLProvider) *ProviderConfig

GetProviderConfig returns the configuration template for a provider

type RefreshTokenRequest

type RefreshTokenRequest struct {
	Token string `json:"token" binding:"required"`
}

RefreshTokenRequest represents a token refresh request

type SAMLAuthenticator

type SAMLAuthenticator struct {
	// contains filtered or unexported fields
}

SAMLAuthenticator handles SAML authentication

func NewSAMLAuthenticator

func NewSAMLAuthenticator(config *SAMLConfig) (*SAMLAuthenticator, error)

NewSAMLAuthenticator creates a new SAML authenticator and initializes the Service Provider (SP) configuration.

This function performs the critical setup for SAML authentication: 1. Validates configuration 2. Creates the SAML Service Provider 3. Loads Identity Provider metadata 4. Initializes SAML middleware 5. Configures security settings

INITIALIZATION STEPS:

STEP 1: Configuration Validation

Checks that SAML is enabled and required fields are present. Returns error if SAML is disabled to prevent misconfiguration.

STEP 2: Entity ID Parsing

The Entity ID must be a valid URL (typically your base domain):

The Entity ID becomes the base for SAML endpoint URLs:

  • Metadata: {EntityID}/saml/metadata
  • ACS: {EntityID}/saml/acs
  • SLO: {EntityID}/saml/slo

STEP 3: Service Provider Creation

Creates a SAML Service Provider with:

  • EntityID: Unique identifier for this SP
  • Key/Certificate: For signing and encryption
  • MetadataURL/AcsURL/SloURL: SAML endpoints
  • AllowIDPInitiated: Whether to accept unsolicited assertions
  • ForceAuthn: Whether to require re-authentication

STEP 4: Identity Provider Metadata Loading

IdP metadata contains critical information:

  • SingleSignOnService: Where to send AuthnRequests
  • X509Certificate: IdP's public key for validating signatures
  • NameIDFormat: Supported identifier formats
  • Attributes: Available user attributes

Metadata can be loaded two ways:

A) From URL (recommended for production):

B) From XML (recommended for air-gapped deployments):

  • Parses XML metadata downloaded from IdP
  • No network dependency
  • Benefits: Works in restricted environments
  • Drawback: Must manually update if IdP changes

STEP 5: SAML Middleware Initialization

Creates the crewjam/saml middleware that:

  • Handles SAML request/response processing
  • Validates assertion signatures
  • Manages SAML sessions
  • Provides RequireAccount middleware

SECURITY CONSIDERATIONS:

1. TLS Validation:

  • When fetching metadata from URL, TLS certificate is validated
  • InsecureSkipVerify is set to false (secure default)
  • Prevents man-in-the-middle attacks

2. Signature Validation:

  • All assertions are validated using IdP's certificate from metadata
  • Prevents tampering with user attributes
  • Handled automatically by crewjam/saml library

3. AllowIDPInitiated:

  • If false: Only accept assertions with valid InResponseTo
  • If true: Accept unsolicited assertions from IdP
  • Default false is more secure (prevents CSRF)

4. ForceAuthn:

  • If true: User must authenticate at IdP every time
  • If false: SSO allowed with active IdP session
  • Default false provides better UX

EXAMPLE USAGE:

config := &SAMLConfig{
    Enabled:     true,
    EntityID:    "https://streamspace.example.com",
    MetadataURL: "https://dev-12345.okta.com/metadata",
    Certificate: cert,
    PrivateKey:  key,
    AllowIDPInitiated: false,
    SignRequest: true,
    ForceAuthn:  false,
    AttributeMapping: AttributeMapping{
        Email:    "email",
        Username: "login",
    },
}

auth, err := NewSAMLAuthenticator(config)
if err != nil {
    log.Fatalf("Failed to create SAML authenticator: %v", err)
}

// Use auth.GinMiddleware() to protect routes
router.Use(auth.GinMiddleware())

COMMON ERRORS:

"SAML is not enabled":

  • Config.Enabled is false
  • Solution: Set Enabled=true in configuration

"invalid entity ID":

"failed to fetch IdP metadata":

  • MetadataURL is unreachable
  • Network connectivity issue
  • IdP is down
  • Solution: Check URL, network, try using MetadataXML instead

"failed to parse IdP metadata XML":

  • MetadataXML is invalid or corrupted
  • Solution: Re-download metadata from IdP

"either MetadataURL or MetadataXML must be provided":

  • Both fields are empty
  • Solution: Provide one of the two

func (*SAMLAuthenticator) ExtractUserFromAssertion

func (sa *SAMLAuthenticator) ExtractUserFromAssertion(assertion *saml.Assertion) (*UserInfo, error)

ExtractUserFromAssertion extracts user information from a SAML assertion.

After the IdP authenticates a user, it sends a SAML assertion containing: 1. Subject (NameID): The user's identifier 2. Attribute Statements: User attributes (email, name, groups, etc.) 3. Conditions: When the assertion is valid 4. Signature: Cryptographic proof from IdP

This function parses the assertion and maps SAML attributes to StreamSpace user fields based on the configured AttributeMapping.

SAML ASSERTION STRUCTURE:

A SAML assertion is an XML document that looks like:

<Assertion>
  <Subject>
    <NameID Format="urn:...">user@example.com</NameID>
  </Subject>
  <AttributeStatement>
    <Attribute Name="email">
      <AttributeValue>user@example.com</AttributeValue>
    </Attribute>
    <Attribute Name="groups">
      <AttributeValue>Admins</AttributeValue>
      <AttributeValue>Users</AttributeValue>
    </Attribute>
  </AttributeStatement>
</Assertion>

ATTRIBUTE MAPPING:

Different IdPs use different attribute names, so we use AttributeMapping to configure which SAML attribute corresponds to which user field.

Example for Okta:

AttributeMapping{
    Email:     "email",
    Username:  "login",
    FirstName: "firstName",
    LastName:  "lastName",
    Groups:    "groups",
}

Example for Azure AD:

AttributeMapping{
    Email:     "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
    Username:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
    FirstName: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
    LastName:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
    Groups:    "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups",
}

FALLBACK TO NAMEID:

If no attribute mapping is configured, or the IdP doesn't send the expected attributes, we fall back to using the SAML NameID:

1. NameID as Username:

  • Always use NameID as username if Username attribute not found
  • Ensures we always have a unique identifier

2. NameID as Email:

  • Use NameID as email if: a) Email attribute not found, AND b) NameID format is "emailAddress"
  • Common with Google Workspace and Azure AD

MULTI-VALUED ATTRIBUTES:

Some attributes like "groups" can have multiple values:

<Attribute Name="groups">
  <AttributeValue>Admins</AttributeValue>
  <AttributeValue>Developers</AttributeValue>
  <AttributeValue>Users</AttributeValue>
</Attribute>

We handle this by: - Storing groups as []string (slice of strings) - Storing other multi-valued attributes as []string in Attributes map - Storing single-valued attributes as string in Attributes map

VALIDATION:

After extracting attributes, we validate required fields: - Username: REQUIRED - cannot create user without identifier - Email: OPTIONAL - nice to have but not required - FirstName/LastName: OPTIONAL - for display purposes - Groups: OPTIONAL - for role-based access control

EXAMPLE EXTRACTED USER:

&UserInfo{
    Username:   "john.doe@example.com",
    Email:      "john.doe@example.com",
    FirstName:  "John",
    LastName:   "Doe",
    Groups:     []string{"Admins", "Developers"},
    Attributes: map[string]interface{}{
        "email":     "john.doe@example.com",
        "firstName": "John",
        "lastName":  "Doe",
        "groups":    []string{"Admins", "Developers"},
        "department": "Engineering",
    },
}

COMMON ERRORS:

"assertion is nil":

  • No assertion provided to function
  • Should never happen in normal flow

"username not found in SAML assertion":

  • IdP didn't send username attribute
  • AttributeMapping.Username is incorrect
  • NameID is missing or empty
  • Solution: Check IdP configuration and attribute mapping

func (*SAMLAuthenticator) ExtractUserFromAttributes

func (sa *SAMLAuthenticator) ExtractUserFromAttributes(attributes samlsp.Attributes) (*UserInfo, error)

ExtractUserFromAttributes extracts user information from SAML session attributes.

This is a simpler version of ExtractUserFromAssertion that works with the attributes map returned by SessionWithAttributes.GetAttributes().

The attributes map contains key-value pairs from the SAML assertion's AttributeStatements, already parsed and ready to use.

func (*SAMLAuthenticator) GetMiddleware

func (sa *SAMLAuthenticator) GetMiddleware() *samlsp.Middleware

GetMiddleware returns the SAML middleware

func (*SAMLAuthenticator) GetServiceProvider

func (sa *SAMLAuthenticator) GetServiceProvider() *saml.ServiceProvider

GetServiceProvider returns the SAML Service Provider instance.

func (*SAMLAuthenticator) GinMiddleware

func (sa *SAMLAuthenticator) GinMiddleware() gin.HandlerFunc

GinMiddleware returns a Gin middleware function that enforces SAML authentication.

This middleware protects routes by requiring valid SAML authentication. It: 1. Checks for an active SAML session 2. Validates the session contains a valid assertion 3. Extracts user information from the assertion 4. Stores user info in Gin context for downstream handlers 5. Redirects unauthenticated users to IdP for SSO

USAGE:

// Protect all routes
router.Use(samlAuth.GinMiddleware())

// Protect specific routes
protected := router.Group("/api")
protected.Use(samlAuth.GinMiddleware())
protected.GET("/sessions", listSessions)

// Access user info in handlers
func listSessions(c *gin.Context) {
    user := c.MustGet("user").(*UserInfo)
    fmt.Printf("User %s requested sessions\n", user.Username)
}

AUTHENTICATION FLOW:

Unauthenticated Request:

  1. User requests /api/sessions
  2. Middleware checks for SAML session → not found
  3. Middleware redirects to /saml/login
  4. SAML login redirects to IdP
  5. User authenticates at IdP
  6. IdP POSTs assertion to /saml/acs
  7. ACS creates SAML session (cookie)
  8. User redirected back to /api/sessions
  9. Middleware finds SAML session → success
  10. Handler executes with user context

Authenticated Request:

  1. User requests /api/sessions
  2. Middleware checks for SAML session → found
  3. Middleware validates assertion
  4. Middleware extracts user info
  5. Handler executes with user context

SESSION STORAGE:

SAML sessions are stored in cookies by the crewjam/saml middleware. The cookie contains: - Session ID (encrypted) - Not the full assertion (too large)

The actual assertion is stored server-side in memory or a session store. The middleware retrieves the assertion using the session ID from the cookie.

SECURITY:

1. Session Validation:

  • Ensures session exists and is valid
  • Checks assertion has not expired
  • Validates assertion signature (done by crewjam/saml)

2. HTTPS Required:

  • SAML sessions should only be sent over HTTPS
  • Cookies should have Secure flag in production
  • Prevents session hijacking

3. Session Expiration:

  • Sessions expire based on assertion NotOnOrAfter
  • Typically 5-10 minutes from authentication
  • Forces periodic re-validation with IdP

ERROR HANDLING:

No SAML session:

  • Redirects to IdP for SSO
  • Aborts current request with c.Abort()
  • User will return after authentication

Invalid session:

  • Returns 401 Unauthorized
  • JSON error response
  • User must re-authenticate

Failed user extraction:

  • Returns 401 Unauthorized
  • JSON error with details
  • Usually indicates IdP misconfiguration

func (*SAMLAuthenticator) SetupRoutes

func (sa *SAMLAuthenticator) SetupRoutes(router *gin.Engine)

SetupRoutes registers all SAML-related HTTP endpoints.

This function creates the following SAML endpoints required for SSO: - /saml/metadata: SP metadata for IdP configuration - /saml/acs: Assertion Consumer Service (callback after authentication) - /saml/slo: Single Logout Service - /saml/login: Initiate SSO authentication flow - /saml/logout: Terminate local SAML session

SAML ENDPOINTS OVERVIEW:

These endpoints implement the SAML 2.0 Web Browser SSO Profile:

┌─────────┐         ┌──────────────┐         ┌─────────┐
│ Browser │────────▶│ StreamSpace  │────────▶│   IdP   │
│         │         │ (SP)         │         │         │
└─────────┘         └──────────────┘         └─────────┘
    │                      │                      │
    │  1. GET /saml/login  │                      │
    ├─────────────────────▶│                      │
    │                      │  2. AuthnRequest     │
    │                      ├─────────────────────▶│
    │                      │                      │
    │  3. Redirect to IdP  │                      │
    │◀─────────────────────┤                      │
    │                                             │
    │  4. Authenticate at IdP                     │
    ├────────────────────────────────────────────▶│
    │                                             │
    │  5. POST assertion to /saml/acs             │
    │◀────────────────────────────────────────────┤
    │                      │                      │
    │  6. POST /saml/acs   │                      │
    ├─────────────────────▶│                      │
    │                      │                      │
    │  7. Session created, │                      │
    │     redirect to app  │                      │
    │◀─────────────────────┤                      │

ENDPOINT DETAILS:

1. /saml/metadata (GET):

  • Returns SP metadata XML
  • IdP needs this to configure StreamSpace as a trusted SP
  • Contains: Entity ID, ACS URL, SLO URL, certificate

2. /saml/acs (POST):

  • Assertion Consumer Service
  • Receives SAML assertion from IdP after authentication
  • Validates assertion signature
  • Creates SAML session
  • Redirects to original requested URL

3. /saml/slo (GET/POST):

  • Single Logout Service
  • IdP sends logout request when user logs out
  • Terminates StreamSpace session
  • Can be HTTP-Redirect (GET) or HTTP-POST (POST)

4. /saml/login (GET):

  • Initiates SSO authentication
  • Generates SAML AuthnRequest
  • Redirects browser to IdP
  • Accepts ?return_url parameter for post-auth redirect

5. /saml/logout (GET):

  • Local logout (does not notify IdP)
  • Clears SAML session cookie
  • Redirects to home page

USAGE EXAMPLE:

router := gin.Default()
samlAuth := NewSAMLAuthenticator(config)
samlAuth.SetupRoutes(router)

// Now endpoints are available:
// - https://streamspace.example.com/saml/metadata
// - https://streamspace.example.com/saml/acs
// - https://streamspace.example.com/saml/slo
// - https://streamspace.example.com/saml/login
// - https://streamspace.example.com/saml/logout

IDP CONFIGURATION:

When configuring StreamSpace in your IdP (Okta, Azure AD, etc.):

1. Download SP metadata from: https://streamspace.example.com/saml/metadata 2. Or manually configure:

3. Configure attribute mapping in IdP to send required attributes 4. Test with: https://streamspace.example.com/saml/login

SECURITY CONSIDERATIONS:

1. HTTPS Required:

  • All SAML endpoints must be accessed over HTTPS in production
  • HTTP is only acceptable for local development
  • Prevents MITM attacks on SAML assertions

2. ACS Validation:

  • The /saml/acs endpoint validates assertion signatures
  • Validates assertion timing (NotBefore, NotOnOrAfter)
  • Validates audience (must match Entity ID)
  • All handled by crewjam/saml middleware

3. Return URL Validation:

  • return_url parameter should be validated
  • Prevents open redirect attacks
  • Currently not validated (TODO: add validation)

4. Session Security:

  • Sessions stored in HTTP-only cookies
  • Cookies should have Secure flag (HTTPS-only)
  • Sessions expire based on assertion validity

type SAMLConfig

type SAMLConfig struct {
	Enabled              bool              // Enable SAML authentication
	EntityID             string            // SP entity ID (e.g., "https://streamspace.example.com")
	MetadataURL          string            // URL to fetch IdP metadata (e.g., "https://idp.example.com/metadata")
	MetadataXML          []byte            // Raw IdP metadata XML (alternative to MetadataURL)
	AssertionConsumerURL string            // ACS endpoint where IdP POSTs assertions
	SingleLogoutURL      string            // SLO endpoint for logout requests
	Certificate          *x509.Certificate // SP's X.509 certificate for signing/encryption
	PrivateKey           *rsa.PrivateKey   // SP's RSA private key
	AllowIDPInitiated    bool              // Allow IdP-initiated SSO (default: false for security)
	SignRequest          bool              // Sign AuthnRequests (required by some IdPs)
	ForceAuthn           bool              // Require re-authentication every time (default: false)
	AttributeMapping     AttributeMapping  // Maps SAML attributes to user fields
}

SAMLConfig holds SAML authentication configuration for Service Provider (SP).

This configuration defines how StreamSpace (acting as a SAML Service Provider) integrates with an Identity Provider (IdP) like Okta, Azure AD, or Google Workspace.

ENTITY ID (EntityID):

The Entity ID is a unique identifier for this Service Provider. It's typically the base URL of your StreamSpace deployment:

METADATA LOADING (MetadataURL vs MetadataXML):

You must provide IdP metadata in one of two ways:

1. MetadataURL: URL to fetch IdP metadata (recommended for production)

2. MetadataXML: Raw XML metadata (recommended for air-gapped deployments)

  • Paste the XML content from IdP's metadata download
  • No network dependency
  • Must manually update if IdP configuration changes

ASSERTION CONSUMER SERVICE (AssertionConsumerURL):

The ACS is the endpoint where the IdP POSTs SAML assertions after authentication. This URL must be:

SINGLE LOGOUT (SingleLogoutURL):

The SLO endpoint handles logout requests from the IdP. When a user logs out from the IdP, it notifies all active SPs to terminate their sessions:

CERTIFICATES AND KEYS:

Certificate and PrivateKey are used to: 1. Sign SAML AuthnRequests (if SignRequest=true) 2. Decrypt encrypted SAML assertions (if IdP encrypts) 3. Sign SAML metadata for IdP to verify

Generate with OpenSSL:

openssl req -x509 -newkey rsa:2048 -keyout sp-key.pem -out sp-cert.pem -days 3650 -nodes

SECURITY SETTINGS:

AllowIDPInitiated (default: false):

  • If false: Only accept assertions in response to SP-initiated AuthnRequests
  • If true: Accept unsolicited assertions from IdP (less secure)
  • WHY: Prevents cross-site request forgery attacks
  • Set to true only if you need IdP portal deep links

SignRequest (default: false):

  • If true: Sign all AuthnRequests with SP's private key
  • If false: Send unsigned AuthnRequests
  • WHY: Prevents tampering with authentication requests
  • Enable if your IdP requires signed requests (Okta, Azure AD)

ForceAuthn (default: false):

  • If true: Require user to re-authenticate at IdP every time
  • If false: Allow SSO if user has active IdP session
  • WHY: Use for high-security scenarios requiring fresh authentication
  • Most deployments leave this false for better UX

ATTRIBUTE MAPPING:

Defines which SAML attributes map to StreamSpace user fields. Different IdPs use different attribute names, so this is configurable:

Okta attributes:

Email:     "email"
Username:  "login"
FirstName: "firstName"
LastName:  "lastName"
Groups:    "groups"

Azure AD attributes:

Email:     "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
Username:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
FirstName: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
LastName:  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
Groups:    "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"

type SAMLProvider

type SAMLProvider string

SAMLProvider represents a SAML identity provider configuration

const (
	// Supported SAML providers
	ProviderOkta            SAMLProvider = "okta"
	ProviderAzureAD         SAMLProvider = "azuread"
	ProviderGoogleWorkspace SAMLProvider = "google"
	ProviderAuth0           SAMLProvider = "auth0"
	ProviderKeycloak        SAMLProvider = "keycloak"
	ProviderAuthentik       SAMLProvider = "authentik"
	ProviderGeneric         SAMLProvider = "generic"
)

type SAMLService

type SAMLService interface {
	GetMiddleware() *samlsp.Middleware
	GetServiceProvider() *saml.ServiceProvider
	ExtractUserFromAssertion(assertion *saml.Assertion) (*UserInfo, error)
}

SAMLService defines the interface for SAML operations

type SessionData

type SessionData struct {
	SessionID string    `json:"session_id"`
	UserID    string    `json:"user_id"`
	Username  string    `json:"username"`
	Role      string    `json:"role"`
	OrgID     string    `json:"org_id"` // Organization ID for multi-tenancy
	CreatedAt time.Time `json:"created_at"`
	ExpiresAt time.Time `json:"expires_at"`
	IPAddress string    `json:"ip_address,omitempty"`
	UserAgent string    `json:"user_agent,omitempty"`
}

SessionData represents a stored session

type SessionStore

type SessionStore struct {
	// contains filtered or unexported fields
}

SessionStore manages server-side session tracking in Redis

func NewSessionStore

func NewSessionStore(cache *cache.Cache) *SessionStore

NewSessionStore creates a new session store

func (*SessionStore) ClearAllSessions

func (s *SessionStore) ClearAllSessions(ctx context.Context) error

ClearAllSessions removes all sessions from Redis (force all users to re-login)

func (*SessionStore) CreateSession

func (s *SessionStore) CreateSession(ctx context.Context, session *SessionData, ttl time.Duration) error

CreateSession stores a new session in Redis

func (*SessionStore) DeleteSession

func (s *SessionStore) DeleteSession(ctx context.Context, sessionID string) error

DeleteSession removes a session from Redis (logout)

func (*SessionStore) DeleteUserSessions

func (s *SessionStore) DeleteUserSessions(ctx context.Context, userID string) error

DeleteUserSessions removes all sessions for a specific user

func (*SessionStore) GetSession

func (s *SessionStore) GetSession(ctx context.Context, sessionID string) (*SessionData, error)

GetSession retrieves a session from Redis

func (*SessionStore) IsEnabled

func (s *SessionStore) IsEnabled() bool

IsEnabled returns whether session tracking is enabled

func (*SessionStore) RefreshSession

func (s *SessionStore) RefreshSession(ctx context.Context, sessionID string, newExpiresAt time.Time) error

RefreshSession extends the TTL of an existing session

func (*SessionStore) ValidateSession

func (s *SessionStore) ValidateSession(ctx context.Context, sessionID string) (bool, error)

ValidateSession checks if a session exists and is valid

type TokenHasher

type TokenHasher struct {
	// contains filtered or unexported fields
}

TokenHasher handles secure token generation and hashing

func NewTokenHasher

func NewTokenHasher() *TokenHasher

NewTokenHasher creates a new token hasher

func (*TokenHasher) GenerateAPIToken

func (t *TokenHasher) GenerateAPIToken() (plainToken string, hashedToken string, err error)

GenerateAPIToken generates an API token (uses bcrypt for better security) Returns plain token and bcrypt hash

func (*TokenHasher) GenerateSecureToken

func (t *TokenHasher) GenerateSecureToken(length int) (plainToken string, hashedToken string, err error)

GenerateSecureToken generates a cryptographically secure random token Returns the plain token (for giving to user) and the hashed token (for storage)

func (*TokenHasher) GenerateSessionToken

func (t *TokenHasher) GenerateSessionToken() (plainToken string, hashedToken string, err error)

GenerateSessionToken generates a session-specific token Returns plain token and SHA256 hash (faster for session validation)

func (*TokenHasher) HashToken

func (t *TokenHasher) HashToken(token string) (string, error)

HashToken hashes a token using bcrypt for secure storage bcrypt is intentionally slow to prevent brute force attacks

func (*TokenHasher) HashTokenSHA256

func (t *TokenHasher) HashTokenSHA256(token string) string

HashTokenSHA256 provides a faster hash for session tokens where lookup speed is critical Use this for session tokens that need fast validation Note: Less secure than bcrypt for password-like tokens, but acceptable for session tokens

func (*TokenHasher) VerifyToken

func (t *TokenHasher) VerifyToken(plainToken, hashedToken string) bool

VerifyToken verifies a plain token against a hashed token

func (*TokenHasher) VerifyTokenSHA256

func (t *TokenHasher) VerifyTokenSHA256(plainToken, hashedToken string) bool

VerifyTokenSHA256 verifies a token against a SHA256 hash

type TokenManager

type TokenManager interface {
	GenerateTokenWithContext(ctx context.Context, userID, username, email, role string, groups []string, ipAddress, userAgent string) (string, error)
	RefreshToken(token string) (string, error)
	ValidateToken(token string) (*Claims, error)
	InvalidateSession(ctx context.Context, sessionID string) error
	GetTokenDuration() time.Duration
}

TokenManager defines the interface for JWT operations

type User

type User struct {
	ID       string   `json:"id"`
	Username string   `json:"username"`
	Email    string   `json:"email"`
	Provider string   `json:"provider"`
	Groups   []string `json:"groups,omitempty"`
}

User represents a user in the system

type UserInfo

type UserInfo struct {
	Username   string                 `json:"username"`
	Email      string                 `json:"email"`
	FirstName  string                 `json:"first_name"`
	LastName   string                 `json:"last_name"`
	Groups     []string               `json:"groups"`
	Attributes map[string]interface{} `json:"attributes"`
}

UserInfo represents extracted user information from SAML

type UserManager

type UserManager interface {
	CreateOrUpdateOIDCUser(ctx context.Context, userInfo *OIDCUserInfo) (*User, error)
}

UserManager interface for OIDC user management

type UserStore

type UserStore interface {
	VerifyPassword(ctx context.Context, username, password string) (*models.User, error)
	GetUser(ctx context.Context, id string) (*models.User, error)
	GetUserGroups(ctx context.Context, userID string) ([]string, error)
	GetUserByEmail(ctx context.Context, email string) (*models.User, error)
	CreateUser(ctx context.Context, req *models.CreateUserRequest) (*models.User, error)
	UpdateUser(ctx context.Context, userID string, req *models.UpdateUserRequest) error
	UpdatePassword(ctx context.Context, userID, password string) error
	AddUserToGroup(ctx context.Context, userID, groupName string) error
	DB() *sql.DB // Kept for backward compatibility if needed, but ideally should be removed
}

UserStore defines the interface for user database operations

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL