lightscale-admin/backend/src/oidc.rs
2026-02-13 17:07:42 +09:00

109 lines
3.4 KiB
Rust

use crate::config::OidcProviderConfig;
use anyhow::{anyhow, Result};
use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreResponseType};
use openidconnect::reqwest::async_http_client;
use openidconnect::{
AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, Scope,
};
use std::collections::HashMap;
#[derive(Clone)]
pub struct OidcProvider {
pub id: String,
pub name: String,
pub client: CoreClient,
pub scopes: Vec<String>,
pub extra_params: HashMap<String, String>,
}
pub async fn load_providers(
providers: &[OidcProviderConfig],
base_url: &str,
) -> Result<HashMap<String, OidcProvider>> {
let mut out = HashMap::new();
for provider in providers {
let issuer = IssuerUrl::new(provider.issuer_url.clone())?;
let metadata = CoreProviderMetadata::discover_async(issuer, async_http_client).await?;
let redirect = format!(
"{}/admin/api/auth/oidc/{}/callback",
base_url.trim_end_matches('/'),
provider.id
);
let client = CoreClient::from_provider_metadata(
metadata,
ClientId::new(provider.client_id.clone()),
Some(ClientSecret::new(provider.client_secret.clone())),
)
.set_redirect_uri(RedirectUrl::new(redirect)?);
out.insert(
provider.id.clone(),
OidcProvider {
id: provider.id.clone(),
name: provider.name.clone(),
client,
scopes: provider.scopes.clone(),
extra_params: provider.extra_params.clone(),
},
);
}
Ok(out)
}
pub struct OidcAuthRequest {
pub url: String,
pub state: String,
pub nonce: String,
pub verifier: String,
}
pub fn build_auth_request(provider: &OidcProvider) -> Result<OidcAuthRequest> {
let (challenge, verifier) = PkceCodeChallenge::new_random_sha256();
let mut request = provider
.client
.authorize_url(
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
CsrfToken::new_random,
Nonce::new_random,
)
.set_pkce_challenge(challenge);
if provider.scopes.is_empty() {
request = request.add_scope(Scope::new("openid".to_string()));
request = request.add_scope(Scope::new("email".to_string()));
request = request.add_scope(Scope::new("profile".to_string()));
} else {
for scope in &provider.scopes {
request = request.add_scope(Scope::new(scope.clone()));
}
}
for (key, value) in &provider.extra_params {
request = request.add_extra_param(key, value);
}
let (url, csrf, nonce) = request.url();
Ok(OidcAuthRequest {
url: url.to_string(),
state: csrf.secret().to_string(),
nonce: nonce.secret().to_string(),
verifier: verifier.secret().to_string(),
})
}
pub async fn exchange_code(
provider: &OidcProvider,
code: String,
verifier: String,
) -> Result<openidconnect::core::CoreTokenResponse> {
let token = provider
.client
.exchange_code(AuthorizationCode::new(code))
.set_pkce_verifier(PkceCodeVerifier::new(verifier))
.request_async(async_http_client)
.await
.map_err(|err| anyhow!("oidc token exchange failed: {}", err))?;
Ok(token)
}