# authentication-patterns > OAuth-first authentication patterns for staff applications. Covers Google/Microsoft OAuth setup, cookie separation, first-user admin pattern, and domain restriction. Use when: implementing login, OAuth, authentication, or user provisioning. - Author: propertymanager-dot - Repository: propertymanager-dot/claude-code-skills - Version: 20260121111928 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/propertymanager-dot/claude-code-skills - Web: https://mule.run/skillshub/@@propertymanager-dot/claude-code-skills~authentication-patterns:20260121111928 --- --- name: authentication-patterns description: > OAuth-first authentication patterns for staff applications. Covers Google/Microsoft OAuth setup, cookie separation, first-user admin pattern, and domain restriction. Use when: implementing login, OAuth, authentication, or user provisioning. --- # Authentication Patterns **Purpose:** OAuth-first authentication for staff applications **Size:** ~4 KB --- ## ⚡ LOAD THIS SKILL WHEN - "OAuth", "Google login", "Microsoft login" - "authentication", "login page", "sign in" - "user provisioning", "first user" - "domain restriction", "allowed domain" --- ## OAuth-Only Authentication (Preferred) Local username/password is **deprecated** for staff applications. ### Benefits | OAuth | Local Auth | |-------|------------| | No password storage | Must secure passwords | | Enterprise identity | Separate credentials | | Auto provisioning | Manual user creation | | Domain restriction | Custom validation | ### When Local Auth is Acceptable - Development/testing without OAuth configured - No internet access - Standalone tools without enterprise integration Even then, OAuth should be primary; local auth as fallback only. --- ## Implementation Patterns ### Login Page: OAuth Only ```html

Sign In

Google Sign in with Google
``` ### First User = Admin ```python from app.models import User, UserRole user = User( email=email, name=name, role=UserRole.ADMIN if db.query(User).count() == 0 else UserRole.VIEWER, auth_provider="google", is_active=True ) ``` ### Domain Restriction ```python GOOGLE_ALLOWED_DOMAIN = os.getenv("GOOGLE_ALLOWED_DOMAIN", "") if GOOGLE_ALLOWED_DOMAIN: email_domain = email.split("@")[1] if email_domain != GOOGLE_ALLOWED_DOMAIN: raise HTTPException(403, f"Only @{GOOGLE_ALLOWED_DOMAIN} accounts allowed") ``` --- ## ⚠️ CRITICAL: Cookie Separation OAuth state and auth session **MUST** use different cookies. ### Wrong (Causes Login Loops) ```python # Same cookie for both - BREAKS LOGIN app.add_middleware( SessionMiddleware, session_cookie=settings.SESSION_COOKIE_NAME, # Conflicts! ) ``` ### Correct ```python # Separate cookies AUTH_SESSION_COOKIE = f"{APP_NAME}_session" # For auth OAUTH_STATE_COOKIE = f"{APP_NAME}_oauth_state" # For OAuth state app.add_middleware( SessionMiddleware, session_cookie=OAUTH_STATE_COOKIE, # OAuth state only ) ``` ### Symptom of Cookie Conflict Login appears successful → immediately redirects back to login page. --- ## Google OAuth Setup ### Environment Variables ```env GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-secret GOOGLE_ALLOWED_DOMAIN=ucc-austin.org BASE_URL=https://pms.ucc-austin.org ``` ### Redirect URI Configuration In Google Cloud Console → Credentials → OAuth 2.0 Client: ``` Authorized redirect URIs: https://pms.ucc-austin.org/oauth/google/callback http://localhost:8008/oauth/google/callback (for testing) ``` ### Private IP Rejection Google OAuth **blocks** redirects to private IPs (192.168.x.x, 10.x.x.x). | Scenario | Solution | |----------|----------| | Local dev | Use `localhost` not `192.168.x.x` | | Production | Use public domain with Caddy | | Internal access | Add hosts file entry (requires admin) | **Hosts file modification requires Administrator privileges.** See `windows-app-build` skill for auto-elevation pattern. --- ## Regression Tests ```python def test_oauth_cookie_separated(): """OAuth state and auth session must use different cookies.""" main_py = Path('app/main.py').read_text() if 'SessionMiddleware' in main_py: assert 'session_cookie=settings.SESSION_COOKIE_NAME' not in main_py, \ "OAuth state cookie must be separate from auth session cookie" def test_login_template_oauth_only(): """Login page should not have password form for OAuth-only apps.""" login_html = Path('app/templates/public/login.html').read_text() if 'google_oauth_enabled' in login_html: has_password = 'type="password"' in login_html has_divider = 'or sign in with' in login_html.lower() if not has_password: assert not has_divider, "Remove divider for OAuth-only" def test_first_user_admin_pattern(): """First OAuth user should become admin.""" oauth_routes = Path('app/routes/oauth_routes.py').read_text() assert 'count() == 0' in oauth_routes or 'first user' in oauth_routes.lower(), \ "OAuth should assign ADMIN to first user" ``` --- ## Checklist ### OAuth Implementation - [ ] Login page has OAuth button only (no password form) - [ ] No "or sign in with" divider for OAuth-only - [ ] OAuth state cookie differs from auth session cookie - [ ] First user gets ADMIN role automatically - [ ] GOOGLE_ALLOWED_DOMAIN is set (not empty) - [ ] Redirect URI registered in Google Console ### Testing - [ ] Login → callback → dashboard works - [ ] Domain restriction rejects other domains - [ ] First user is admin, second user is viewer - [ ] Logout clears session properly --- *End of Authentication Patterns Skill*