SSO Concepts: OAuth2 and OpenID Connect¶
Understanding Single Sign-On, OAuth2, and OIDC for authentication.
What is SSO?¶
Single Sign-On (SSO) allows users to authenticate once and access multiple applications without re-entering credentials.
flowchart TD
A[User logs in to Identity Provider] --> B[IdP issues tokens]
B --> C[User accesses App A]
B --> D[User accesses App B]
B --> E[User accesses App C]
style C fill:#e1f5fe
style D fill:#e1f5fe
style E fill:#e1f5fe
OAuth2 vs OpenID Connect¶
| Protocol | Purpose | Use Case |
|---|---|---|
| OAuth2 | Authorization | "Can this app access my photos?" |
| OIDC | Authentication | "Who is this user?" |
OpenID Connect (OIDC) is built on top of OAuth2, adding identity layer.
The OIDC Flow¶
Authorization Code Flow (Recommended for Web Apps)¶
sequenceDiagram
participant User
participant Browser
participant App
participant IdP
User->>Browser: Click "Login"
Browser->>App: GET /login
App-->>Browser: Redirect to IdP
Browser->>IdP: GET /authorize?client_id=webapp&...
IdP->>User: Show login form
User->>IdP: Enter credentials
IdP-->>Browser: Redirect with code
Browser->>App: GET /callback?code=abc123&state=...
App->>IdP: POST /token (exchange code)
IdP-->>App: access_token, id_token, refresh_token
App->>App: Decode id_token / call /userinfo
App-->>Browser: Set session cookie
Tokens¶
ID Token¶
JWT containing user identity claims. Decoded by the application.
{
"iss": "https://auth.example.com",
"sub": "user-123-uuid",
"aud": "webapp",
"exp": 1704067200,
"iat": 1704063600,
"email": "alice@example.com",
"name": "Alice Smith",
"email_verified": true
}
Key claims:
| Claim | Description |
|---|---|
| iss | Issuer (IdP URL) |
| sub | Subject (unique user ID) |
| aud | Audience (client ID) |
| exp | Expiration timestamp |
| iat | Issued at timestamp |
Access Token¶
Used to call APIs. Often opaque (not a JWT). Short-lived.
Refresh Token¶
Used to obtain new access tokens without re-authentication. Long-lived.
Session Management¶
After OIDC flow completes, store user identity in session:
@app.get("/auth/callback")
async def callback(request: Request):
token = await oauth.authentik.authorize_access_token(request)
userinfo = token.get('userinfo')
request.session['user'] = {
'sub': userinfo['sub'],
'email': userinfo['email'],
'name': userinfo.get('name', userinfo['email']),
}
return RedirectResponse(url="/")
Cookie-Based Sessions¶
app.add_middleware(
SessionMiddleware,
secret_key=os.environ['SESSION_SECRET'],
max_age=86400, # 24 hours
https_only=True,
same_site='lax',
)
Session Data¶
The browser stores a signed cookie containing user identity:
When decoded, it contains:
Integration with Authorization¶
Authentication provides the identity. Authorization uses it.
def identity_provider(request: Request) -> Identity:
"""Extract identity from OIDC session."""
user = request.session.get('user')
if not user:
return Identity(type=IdentityType.IDENTITY_TYPE_NONE)
return Identity(
type=IdentityType.IDENTITY_TYPE_SUB,
value=user['sub'],
)
topaz_config = TopazConfig(
identity_provider=identity_provider,
...
)
Identity Providers¶
Authentik¶
Open-source IdP with rich features.
oauth.register(
name='authentik',
server_metadata_url='https://authentik.example.com/application/o/app/.well-known/openid-configuration',
client_id='...',
client_secret='...',
)
Keycloak¶
Enterprise-grade Java-based IdP.
oauth.register(
name='keycloak',
server_metadata_url='https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration',
)
Auth0¶
Cloud IdP with extensive integrations.
oauth.register(
name='auth0',
server_metadata_url='https://tenant.auth0.com/.well-known/openid-configuration',
)
Security Considerations¶
| Concern | Mitigation |
|---|---|
| Token theft | Short expiration, HTTPS only |
| CSRF | State parameter in OIDC flow |
| Session fixation | Regenerate session on login |
| XSS | HttpOnly cookies |
| Open redirect | Validate redirect URIs |
State Parameter¶
Prevents CSRF attacks by ensuring the callback matches the request:
# Login: Generate random state, store in session
state = secrets.token_urlsafe(32)
request.session['oauth_state'] = state
# Callback: Verify state matches
if request.query_params['state'] != request.session['oauth_state']:
raise HTTPException(400, "Invalid state")
Authlib handles this automatically.
Logout¶
Application Logout¶
Clear local session:
@app.get("/logout")
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url="/")
Single Logout (SLO)¶
Also log out from IdP:
@app.get("/logout")
async def logout(request: Request):
request.session.clear()
return RedirectResponse(url=f"{OIDC_ISSUER}/logout?post_logout_redirect_uri={APP_URL}")
See Also¶
- Authentication Tutorial - Step-by-step guide
- OIDC Reference - Configuration options
- Authentik Setup - IdP configuration