# Integration Guide Practical guide to integrating your applications with the Auth Service. ## Overview The Auth Service uses **OAuth2 with Google** for authentication and issues **JWTs signed with RS256**. Applications validate tokens locally using the service's public key via JWKS. ## Architecture ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Your App │ │ Auth Service │ │ Google OAuth │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ │ 1. /login?redirect= │ │ │ ───────────────────► │ │ │ │ 2. OAuth redirect │ │ │ ───────────────────► │ │ │ │ │ │ 3. code + user info │ │ │ ◄─────────────────── │ │ │ │ │ 4. redirect + JWT │ │ │ ◄─────────────────── │ │ │ (cookie + token) │ │ │ │ │ │ 5. Validate JWT │ │ │ (uses JWKS) │ │ ``` ## Basic Integration Flow 1. Redirect user to `/login` with callback URL 2. Receive JWT token after authentication 3. Validate token locally using JWKS 4. Use authenticated user information ## Endpoints ### Login ``` GET https://auth.yourdomain.com/login?redirect={callback_url}&mode={mode} ``` | Parameter | Required | Description | |------------|----------|--------------------------------------------------| | `redirect` | Yes | Callback URL after authentication | | `mode` | No | `cookie`, `token`, or `both` (default: `cookie`) | | `state` | No | Opaque value returned in callback | ### Logout ``` GET https://auth.yourdomain.com/logout?redirect={url} ``` ### JWKS (Public Keys) ``` GET https://auth.yourdomain.com/.well-known/jwks.json ``` Cache this response for 24 hours. ### User Info ``` GET https://auth.yourdomain.com/userinfo Authorization: Bearer {token} ``` ### Refresh Token ``` POST https://auth.yourdomain.com/refresh Authorization: Bearer {token} ``` ## JWT Payload ```json { "iss": "https://auth.yourdomain.com", "sub": "google-oauth2|123456789", "iat": 1706097600, "email": "user@gmail.com", "name": "User Name", "picture": "https://..." } ``` | Field | Description | |-------|-------------| | `iss` | Issuer (always your auth service domain) | | `sub` | Unique user ID (`provider|id`) | | `iat` | Issuance timestamp (Unix) | | `email` | User email | | `name` | User name | | `picture` | Profile photo URL | **Note**: No `exp` field. Each app decides validity based on `iat`. ## Token Validation ### Algorithm 1. Fetch JWKS (cache for 24h) 2. Extract token from `__auth` cookie or `Authorization` header 3. Decode header to get `kid` 4. Validate signature with correct public key 5. Validate issuer (`iss`) 6. Validate token age (app policy) ### Code Examples #### TypeScript/Node.js ```typescript import { jwtVerify, createRemoteJWKSet } from 'jose'; const JWKS = createRemoteJWKSet( new URL('https://auth.yourdomain.com/.well-known/jwks.json') ); async function validateToken(token: string) { const { payload } = await jwtVerify(token, JWKS, { issuer: 'https://auth.yourdomain.com', }); // Validate age (7 days) const age = Date.now() / 1000 - payload.iat; if (age > 7 * 24 * 60 * 60) { throw new Error('Token expired'); } return payload; } ``` #### Python ```python import jwt from jwt import PyJWKClient import time jwks_client = PyJWKClient('https://auth.yourdomain.com/.well-known/jwks.json') def validate_token(token: str): signing_key = jwks_client.get_signing_key_from_jwt(token) payload = jwt.decode( token, key=signing_key.key, algorithms=['RS256'], issuer='https://auth.yourdomain.com' ) # Validate age (7 days) if time.time() - payload['iat'] > 7 * 24 * 60 * 60: raise ValueError('Token expired') return payload ``` #### Go ```go package main import ( "github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5/request" ) func validateToken(tokenString string) (jwt.MapClaims, error) { parser := jwt.NewParser(jwt.WithoutClaimsValidation) token, err := parser.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Fetch public key from JWKS return getPublicKeyFromJWKS(token.Header["kid"]) }) if err != nil { return nil, err } claims := token.Claims.(jwt.MapClaims) // Validate issuer if claims["iss"] != "https://auth.yourdomain.com" { return nil, errors.New("invalid issuer") } // Validate age iat := int64(claims["iat"].(float64)) maxAge := int64(7 * 24 * 60 * 60) if time.Now().Unix()-iat > maxAge { return nil, errors.New("token expired") } return claims, nil } ``` ## Integration Patterns ### Pattern 1: Cookie-Based (Subdomains) Use `cookie` mode for automatic authentication between subdomains. **Best for**: Web applications on subdomains of your domain. ```typescript // Redirect to login window.location.href = 'https://auth.yourdomain.com/login?redirect=' + encodeURIComponent(window.location.href); // __auth cookie is automatically sent to subdomains ``` **Example: SvelteKit Middleware** ```typescript // src/hooks.server.ts import { jwtVerify, createRemoteJWKSet } from 'jose'; const JWKS = createRemoteJWKSet( new URL('https://auth.yourdomain.com/.well-known/jwks.json') ); export async function handle({ event, resolve }) { const token = event.cookies.get('__auth'); if (token) { try { const { payload } = await jwtVerify(token, JWKS, { issuer: 'https://auth.yourdomain.com' }); // Validate age (7 days) const age = Date.now() / 1000 - payload.iat; if (age < 7 * 24 * 60 * 60) { event.locals.user = payload; } } catch (e) { // Invalid token, ignore } } return resolve(event); } ``` ### Pattern 2: Token-Based (External Apps) Use `token` mode for apps without cookie access. **Best for**: Mobile apps, external APIs, single-page apps. ```typescript // Redirect to login const redirectUrl = `https://myapp.com/callback?state=${state}`; window.location.href = `https://auth.yourdomain.com/login?` + `redirect=${encodeURIComponent(redirectUrl)}&mode=token`; // In callback, extract token from URL const params = new URLSearchParams(window.location.search); const token = params.get('token'); // Store token securely (localStorage, secure storage, etc.) localStorage.setItem('auth_token', token); // Send token in requests fetch('/api/data', { headers: { 'Authorization': `Bearer ${token}` } }); ``` ### Pattern 3: Hybrid (Both) Use `both` mode to get both cookie and token. **Best for**: Web apps that need to work across domains. ## Recommended Libraries | Language | Library | |-----------|---------| | Node.js | `jose` | | Python | `PyJWT` + `cryptography` | | Go | `github.com/golang-jwt/jwt` | | Rust | `jsonwebtoken` | | Kotlin | `java-jwt` | | Java | `java-jwt` | | Ruby | `jwt` | | PHP | `firebase/php-jwt` | | .NET | `System.IdentityModel.Tokens.Jwt` | ## Best Practices 1. **Cache JWKS**: Requests to `/jwks.json` should be cached for 24h 2. **Validate issuer**: Always verify `iss` matches your auth service domain 3. **Expiration policy**: Define a maximum age based on `iat` (e.g., 7 days) 4. **Error handling**: Invalid tokens should not break your app 5. **Refresh**: Use `/refresh` to extend sessions without re-authenticating 6. **Logout**: Redirect to `/logout` with return URL 7. **Secure storage**: Store tokens securely (HttpOnly cookies for web, secure storage for mobile) 8. **Token rotation**: Periodically rotate tokens for enhanced security ## Troubleshooting ### Invalid Token **Possible causes**: - Incorrect issuer - Public key not obtained from JWKS - Wrong algorithm (must be `RS256`) - Token tampered **Solution**: Verify all validation steps and check the JWKS endpoint. ### Expired Token **Solution**: - Redirect to `/login` for fresh authentication - Or use `/refresh` if token is still valid (< 30 days) ### CORS Issues If you need to call `/userinfo` from the browser: **Solution**: Configure CORS on the auth service or use a server-side proxy. ### Cookie Not Sent **Possible causes**: - App not on the same domain - Cookie missing required flags (`Secure`, `HttpOnly`, `SameSite`) **Solution**: Ensure cookie domain configuration matches your deployment. ## Support For questions or issues: 1. Consult [auth-spec.md](auth-spec.md) for technical details 2. Check the [troubleshooting section](#troubleshooting) 3. Open an issue in the repository