//! OIDC device-flow login. //! //! Wire flow (matching CONSOLE_API.md ยง3.5): //! //! 1. `POST /api/oidc/auth { op: , id, uuid, deviceInfo }` โ†’ //! `{ code: , url: }`. The client //! opens `url` in the user's browser. //! 2. The IdP redirects the browser back to our `/oidc/callback?code=...&state=...`. //! That handler exchanges the IdP code for a token, fetches userinfo, //! upserts a local user, mints our own access token, and marks the //! session `success`. //! 3. The client polls `GET /api/oidc/auth-query?code=&id=&uuid=` until it //! sees a wrapped `AuthBody` envelope. //! //! Auth on the IdP side is handled by the provider's standard OAuth2 //! authorization-code flow. We keep the hbbs side minimal: discovery via //! `/.well-known/openid-configuration`, no JWT verification (we //! trust the userinfo endpoint, authenticated via the access token). pub mod auth; pub mod callback; pub mod discovery; pub mod poll; pub mod providers; use crate::api::error::ApiError; use crate::api::state::AppState; use crate::database::OidcProviderRow; const OIDC_SESSION_TTL_SECS: i64 = 600; // 10 minutes โ€” the user has to sign in fast /// Convenience: resolve a provider name to its row, or an ApiError if it /// doesn't exist or is disabled. pub(crate) async fn require_provider( state: &AppState, name: &str, ) -> Result { state .db .oidc_provider_get(name) .await .map_err(|e| ApiError::Internal(e.to_string()))? .ok_or_else(|| ApiError::BadRequest(format!("unknown OIDC provider: {}", name))) } /// 24 random bytes, base64url-encoded โ†’ ~32 characters. Used for both the /// poll-handle (`code`) and the CSRF state. pub(crate) fn random_token() -> String { base64::encode_config( sodiumoxide::randombytes::randombytes(24), base64::URL_SAFE_NO_PAD, ) }