# ADR-012: Cloud Authentication via OAuth/OIDC Greeter
## Status
Accepted
## Context
In cloud and enterprise deployments, users authenticate against a centralized Identity Provider (IdP) rather than local `/etc/passwd` accounts. The WayRay greeter (ADR-010) must support OAuth 2.0 / OpenID Connect flows to bridge cloud identity to a local user context (PAM session, home dir, etc.).
The greeter is the natural integration point -- it runs before any user context exists, talks to the IdP, and triggers local user provisioning + PAM session creation.
The greeter itself needs a Wayland session to display the login UI, but no user is authenticated yet. The session launcher creates a minimal **ephemeral session** running as a service user (e.g., `wrlogin`):
4. User scans QR code or types URL on their phone/laptop
5. User authenticates on their own device (full browser, MFA, etc.)
6. Greeter polls IdP: `POST /token` with `device_code`
7. Once authorized, IdP returns tokens
8. Greeter validates ID token, extracts claims
**Pros:** No browser needed on server. User authenticates on their own device with full capabilities. Perfect for thin clients. Works with any MFA method.
**Cons:** Requires user to have a second device (phone). Polling adds a few seconds.
### Flow 3: Resource Owner Password (Legacy)
Direct username/password POST to IdP token endpoint. Simple but:
- No MFA support
- IdP must enable this flow (many disable it)
- Only for legacy/migration scenarios
## Identity-to-User Mapping
The greeter/session-launcher must map IdP claims to a local user:
### Claim Mapping Configuration
```toml
# /etc/wayray/auth.toml
[oidc]
issuer = "https://idp.example.com"
client_id = "wayray-server"
# client_secret via env var or file reference
client_secret_file = "/etc/wayray/oidc-secret"
# Which flow to use
flow = "device_code" # or "authorization_code"
# Scopes to request
scopes = ["openid", "profile", "email", "groups"]
[mapping]
# How to derive the local username from IdP claims
username_claim = "preferred_username"
# Fallback: derive from email (strip @domain)
username_fallback = "email_local_part"
# Group mapping: IdP group claim -> local groups
groups_claim = "groups"
[mapping.group_map]
"cloud-admins" = "wheel"
"developers" = "staff"
"users" = "users"
[provisioning]
# Auto-create local user on first login?
auto_provision = true
# Home directory template
home_template = "/export/home/{username}"
# Default shell
default_shell = "/usr/bin/bash"
# Default groups for new users
default_groups = ["users"]
# Create home dir via
home_create = "zfs" # "zfs" (create ZFS dataset) or "mkdir" or "autofs"
2. Session launcher checks if local user `jdoe` exists
3. If not: create user via `useradd` (or illumos equivalent), create home dir (ZFS dataset), set groups
4. Open PAM session for `jdoe`
5. Start desktop session
### Subsequent Logins
1. IdP claims arrive
2. Local user exists → update group memberships if changed
3. Open PAM session
4. Resume or create desktop session
## PAM Integration
The session launcher uses PAM, not the greeter directly. Two PAM paths:
### Path A: `pam_oidc` module
A PAM module that validates OIDC tokens. The greeter passes the ID token to the session launcher, which passes it to PAM. PAM validates the token signature, checks claims, and opens the session.
### Path B: PAM bypass with pre-validated token
The greeter validates the OIDC token itself (signature, issuer, audience, expiry). It then tells the session launcher "user `jdoe` is authenticated" and the session launcher opens a PAM session using `pam_permit` or a custom module that trusts the greeter's assertion. The token validation happens in the greeter, not in PAM.
**Recommendation:** Path B for simplicity. The greeter is a trusted component (runs on the server). Let it handle OIDC complexity. PAM handles session setup (limits, env, mount).
## Greeter as Integration Tool
The greeter's role expands in cloud deployments:
| Responsibility | Local Mode | Cloud Mode |
|---------------|-----------|------------|
| Display login UI | Username + password form | Device code + QR, or browser |
| Authenticate | PAM directly | OAuth/OIDC with IdP |
| Identity mapping | N/A (local user) | Claims → local user |
| User provisioning | N/A (pre-existing) | Auto-create on first login |
├── auth-oidc (OAuth/OIDC device code or auth code)
├── auth-smartcard (PC/SC → certificate → PAM or IdP)
└── auth-kerberos (kinit-style → PAM)
```
The greeter UI presents available methods based on configuration. Each plugin implements:
```rust
trait AuthPlugin {
/// Display name for the login screen
fn display_name(&self) -> &str;
/// Start the authentication flow, returning UI elements to render
fn start(&mut self) -> AuthUI;
/// Poll for completion (for async flows like device code)
fn poll(&mut self) -> AuthState;
/// Returns the authenticated identity on success
fn identity(&self) -> Option<AuthenticatedUser>;
}
enum AuthState {
Pending,
NeedsInput(Vec<AuthField>), // username, password, PIN, etc.
Success(AuthenticatedUser),
Failed(String),
}
struct AuthenticatedUser {
username: String,
uid: Option<u32>, // if already resolved
groups: Vec<String>,
token: Option<String>, // OIDC token for session launcher
claims: HashMap<String,String>,
}
```
## Rationale
- **Device code flow is ideal for thin clients**: No browser on server, user authenticates on their own device with full MFA support. QR code makes it frictionless.
- **Greeter as integration point**: It already exists (ADR-010), it runs pre-auth, it has a UI. Adding OIDC is a natural extension, not a new component.
- **Plugin architecture**: Different deployments need different auth. Enterprise wants OIDC + smart card. University wants LDAP. Home user wants password. Don't force one choice.
- **Auto-provisioning**: Cloud users shouldn't need a sysadmin to create their Unix account. Map claims to user, create on first login.
- **Session launcher interface unchanged**: `Greeter → "user X authenticated" → Launcher`. The launcher doesn't care HOW authentication happened.
## Consequences
- Greeter becomes more complex (OIDC client library, QR code rendering, polling)
- Need an OIDC client library in Rust (openidconnect crate)
- Device code flow adds ~5-15 seconds to first login (user must authenticate on phone)
- Auto-provisioning needs careful security review (who can get an account?)
- Group mapping configuration can be complex for large orgs
- Token refresh: long-running sessions may need token refresh for access to IdP-gated resources
- The ephemeral pre-auth session is a new concept that needs careful lifecycle management