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:
- App generates state parameter (CSRF protection)
- App redirects user to IdP's authorization endpoint
- URL includes: client_id, redirect_uri, scope, state
- Example: https://accounts.google.com/o/oauth2/v2/auth?client_id=...
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:
- IdP redirects back to app with authorization code
- URL: https://streamspace.example.com/auth/oidc/callback?code=abc123&state=xyz
- App validates state matches (CSRF protection)
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:
- User visits StreamSpace (Service Provider)
- User clicks "Login with SSO"
- SP generates SAML AuthnRequest (authentication request)
- User's browser redirects to IdP with AuthnRequest
- User authenticates with IdP (username/password, MFA, etc.)
- IdP generates SAML Assertion (signed XML with user attributes)
- User's browser POSTs assertion to SP's Assertion Consumer Service (ACS)
- SP validates assertion signature and extracts user attributes
- SP creates local session and issues JWT token
- 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
- func CompareAPIKey(key, hash string) bool
- func GenerateAPIKey() (string, error)
- func GenerateSessionID() (string, error)
- func GetUserID(c *gin.Context) (string, bool)
- func GetUserRole(c *gin.Context) (string, bool)
- func GetUsername(c *gin.Context) (string, bool)
- func HashAPIKey(key string) (string, error)
- func IsAdmin(c *gin.Context) bool
- func IsOperator(c *gin.Context) bool
- func LoadCertificate(certPath string) (*x509.Certificate, error)
- func LoadPrivateKey(keyPath string) (*rsa.PrivateKey, error)
- func Middleware(jwtManager *JWTManager, userDB *db.UserDB) gin.HandlerFunc
- func OptionalAuth(jwtManager *JWTManager, userDB *db.UserDB) gin.HandlerFunc
- func RequireAnyRole(roles ...string) gin.HandlerFunc
- func RequireRole(requiredRole string) gin.HandlerFunc
- func ValidateAPIKeyFormat(key string) error
- func ValidateConfig(config *AuthConfig) error
- type APIKeyMetadata
- type AttributeMapping
- type AuthConfig
- type AuthHandler
- func (h *AuthHandler) ChangePassword(c *gin.Context)
- func (h *AuthHandler) Login(c *gin.Context)
- func (h *AuthHandler) Logout(c *gin.Context)
- func (h *AuthHandler) RefreshToken(c *gin.Context)
- func (h *AuthHandler) RegisterRoutes(router *gin.RouterGroup)
- func (h *AuthHandler) SAMLCallback(c *gin.Context)
- func (h *AuthHandler) SAMLLogin(c *gin.Context)
- func (h *AuthHandler) SAMLMetadata(c *gin.Context)
- type AuthMode
- type Claims
- type JWTConfig
- type JWTManager
- func (m *JWTManager) ClearAllSessions(ctx context.Context) error
- func (m *JWTManager) ExtractUserID(tokenString string) (string, error)
- func (m *JWTManager) GenerateToken(userID, username, email, role string, groups []string) (string, error)
- func (m *JWTManager) GenerateTokenWithContext(ctx context.Context, userID, username, email, role string, groups []string, ...) (string, error)
- func (m *JWTManager) GenerateTokenWithOrg(ctx context.Context, userID, username, email, role string, groups []string, ...) (string, error)
- func (m *JWTManager) GetSessionStore() *SessionStore
- func (m *JWTManager) GetTokenDuration() time.Duration
- func (m *JWTManager) InvalidateSession(ctx context.Context, sessionID string) error
- func (m *JWTManager) InvalidateUserSessions(ctx context.Context, userID string) error
- func (m *JWTManager) RefreshToken(tokenString string) (string, error)
- func (m *JWTManager) SetSessionStore(store *SessionStore)
- func (m *JWTManager) ValidateSession(ctx context.Context, sessionID string) (bool, error)
- func (m *JWTManager) ValidateToken(tokenString string) (*Claims, error)
- type LoginRequest
- type LoginResponse
- type OIDCAuthenticator
- func (a *OIDCAuthenticator) GetAuthorizationURL(state string) string
- func (a *OIDCAuthenticator) GetDiscoveryDocument() (map[string]interface{}, error)
- func (a *OIDCAuthenticator) HandleCallback(ctx context.Context, code string) (*OIDCUserInfo, error)
- func (a *OIDCAuthenticator) OIDCCallbackHandler(userManager UserManager) gin.HandlerFunc
- func (a *OIDCAuthenticator) OIDCLoginHandler(c *gin.Context)
- type OIDCConfig
- type OIDCProvider
- type OIDCProviderConfig
- type OIDCUserInfo
- type OrgInfo
- type PasswordChangeRequest
- type ProviderConfig
- type RefreshTokenRequest
- type SAMLAuthenticator
- func (sa *SAMLAuthenticator) ExtractUserFromAssertion(assertion *saml.Assertion) (*UserInfo, error)
- func (sa *SAMLAuthenticator) ExtractUserFromAttributes(attributes samlsp.Attributes) (*UserInfo, error)
- func (sa *SAMLAuthenticator) GetMiddleware() *samlsp.Middleware
- func (sa *SAMLAuthenticator) GetServiceProvider() *saml.ServiceProvider
- func (sa *SAMLAuthenticator) GinMiddleware() gin.HandlerFunc
- func (sa *SAMLAuthenticator) SetupRoutes(router *gin.Engine)
- type SAMLConfig
- type SAMLProvider
- type SAMLService
- type SessionData
- type SessionStore
- func (s *SessionStore) ClearAllSessions(ctx context.Context) error
- func (s *SessionStore) CreateSession(ctx context.Context, session *SessionData, ttl time.Duration) error
- func (s *SessionStore) DeleteSession(ctx context.Context, sessionID string) error
- func (s *SessionStore) DeleteUserSessions(ctx context.Context, userID string) error
- func (s *SessionStore) GetSession(ctx context.Context, sessionID string) (*SessionData, error)
- func (s *SessionStore) IsEnabled() bool
- func (s *SessionStore) RefreshSession(ctx context.Context, sessionID string, newExpiresAt time.Time) error
- func (s *SessionStore) ValidateSession(ctx context.Context, sessionID string) (bool, error)
- type TokenHasher
- func (t *TokenHasher) GenerateAPIToken() (plainToken string, hashedToken string, err error)
- func (t *TokenHasher) GenerateSecureToken(length int) (plainToken string, hashedToken string, err error)
- func (t *TokenHasher) GenerateSessionToken() (plainToken string, hashedToken string, err error)
- func (t *TokenHasher) HashToken(token string) (string, error)
- func (t *TokenHasher) HashTokenSHA256(token string) string
- func (t *TokenHasher) VerifyToken(plainToken, hashedToken string) bool
- func (t *TokenHasher) VerifyTokenSHA256(plainToken, hashedToken string) bool
- type TokenManager
- type User
- type UserInfo
- type UserManager
- type UserStore
Constants ¶
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 ¶
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 ¶
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 ¶
GenerateSessionID creates a cryptographically random session ID
func GetUserRole ¶
GetUserRole extracts the user role from the Gin context
func GetUsername ¶
GetUsername extracts the username from the Gin context
func HashAPIKey ¶
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 IsOperator ¶
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 ¶
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) 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 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 ¶
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):
- Valid: "https://streamspace.example.com"
- Invalid: "streamspace" (not a URL)
- Invalid: "http://localhost" (use HTTPS in production)
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):
- Fetches metadata from IdP's metadata endpoint
- Uses HTTPS with certificate validation
- Example: "https://dev-12345.okta.com/app/abc123/sso/saml/metadata"
- Benefits: Auto-updates when IdP changes configuration
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":
- EntityID is not a valid URL
- Solution: Use full URL like "https://streamspace.example.com"
"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:
- User requests /api/sessions
- Middleware checks for SAML session → not found
- Middleware redirects to /saml/login
- SAML login redirects to IdP
- User authenticates at IdP
- IdP POSTs assertion to /saml/acs
- ACS creates SAML session (cookie)
- User redirected back to /api/sessions
- Middleware finds SAML session → success
- Handler executes with user context
Authenticated Request:
- User requests /api/sessions
- Middleware checks for SAML session → found
- Middleware validates assertion
- Middleware extracts user info
- 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:
- Entity ID: https://streamspace.example.com
- ACS URL: https://streamspace.example.com/saml/acs
- SLO URL: https://streamspace.example.com/saml/slo
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:
- Example: "https://streamspace.example.com"
- Must match the Entity ID configured in your IdP
- Used by IdP to identify which SP is making the request
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)
- Example: "https://dev-12345.okta.com/app/abc123/sso/saml/metadata"
- Automatically updates when IdP configuration changes
- Requires network access to IdP during startup
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:
- Registered in your IdP's SP configuration
- Accessible from user browsers (not internal-only)
- Example: "https://streamspace.example.com/saml/acs"
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:
- Example: "https://streamspace.example.com/saml/slo"
- Optional but recommended for security
- Ensures user is logged out from all services
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 ¶
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 (*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