OAuth Providers ~10 min
Overview
Shell supports multiple identity providers managed in the identity_providers table. Providers can be enabled/disabled at runtime — no restart required.
| Provider type | Examples | Notes |
|---|---|---|
keycloak | Keycloak realm | PKCE S256 flow via KeycloakOAuthService |
google | Google OAuth2 | PKCE S256 via GenericOAuthService |
oauth2 | GitHub, custom | PKCE S256 via GenericOAuthService |
Auto-seeded providers
On startup, Shell auto-registers (upserts) providers from env vars:
Keycloak
Always registered if APP_ENV_KEYCLOAK_CLIENT_ID is set. Uses KeycloakOAuthService (PKCE S256).
Google
Registered when APP_ENV_GOOGLE_CLIENT_ID is set:
APP_ENV_GOOGLE_CLIENT_ID=786155783958-....apps.googleusercontent.com
APP_ENV_GOOGLE_CLIENT_SECRET=GOCSPX-...Upsert on every boot
If Google is already in the DB with old credentials, Shell updates it with the current env values on every startup. Safe to rotate credentials via env.
List enabled providers
GET /api/auth/providersReturns providers with status = ACTIVE. Used by Shell UI to show login buttons.
{
"data": [
{ "id": "uuid", "providerId": "keycloak", "type": "oauth2", "status": "ACTIVE" },
{ "id": "uuid", "providerId": "google", "type": "oauth2", "status": "ACTIVE" }
]
}Register a custom OAuth2 provider
POST /api/config
Authorization: Bearer <shell-jwt> (requires settings:write)
Content-Type: application/json
{
"providerId": "github",
"type": "oauth2",
"clientId": "your-github-client-id",
"clientSecret": "your-github-client-secret",
"authUrl": "https://github.com/login/oauth/authorize",
"tokenUrl": "https://github.com/login/oauth/access_token",
"config": { "scope": "read:user user:email" },
"status": "ACTIVE"
}How the login flow is routed
GET /api/auth/login?provider=google
↓
AuthService.getLoginUrl('google')
↓
ProviderService.getProvider('google')
↓
'keycloak' → KeycloakOAuthService.getAuthorizationUrl() [PKCE S256]
other → GenericOAuthService.getAuthorizationUrl() [PKCE S256]Both use PKCE S256. The code_verifier is stored in Redis with the state key.
Callback routing
GET /api/auth/callback?code=...&state=...
↓
StateStoreService.peekState(state) → { codeVerifier, providerId }
↓
providerId = 'keycloak' → handleKeycloakCallback()
other → handleGenericOAuthCallback()User info extraction
For generic providers, Shell decodes the OIDC id_token to extract:
| Claim | Mapped to |
|---|---|
sub | providerUserId (OAuth account) |
email | users.email |
name | Split into firstName / lastName |
given_name | firstName (preferred) |
family_name | lastName (preferred) |
ID token is not re-verified
Shell trusts the id_token from the provider's token endpoint. The token exchange already verified the authorization code — re-verification of the JWT signature is not performed for generic providers.
Enable / disable a provider
PUT /api/config/:providerId/status
Authorization: Bearer <shell-jwt> (requires settings:write)
Content-Type: application/json
{ "status": "INACTIVE" }Disabled providers no longer appear in GET /api/auth/providers and logins via that provider will be rejected.
Provider model (identity_providers table)
| Field | Description |
|---|---|
providerId | Unique string key: keycloak, google, github, etc. |
type | oauth2, saml, ldap |
clientId | OAuth client ID |
clientSecret | OAuth client secret (stored in DB — use secrets management in prod) |
authUrl | Authorization endpoint URL |
tokenUrl | Token endpoint URL |
config | JSONB — extra config like scope, response_type |
status | ACTIVE | INACTIVE |