use crate::{key, schema, server::session::SessionHelper}; use microrm::prelude::*; use serde::{Deserialize, Serialize}; mod api; mod authorize; mod token; type Request = tide::Request; const AUTHORIZE_PATH: &str = "oidc/authorize"; const TOKEN_PATH: &str = "oidc/token"; const JWKS_PATH: &str = "oidc/jwks"; const DISCOVERY_PATH: &str = ".well-known/openid-configuration"; #[derive(serde::Serialize)] pub enum OIDCErrorType { InvalidRequest, UnauthorizedClient, AccessDenied, UnsupportedResponseType, // InvalidScope, ServerError, // TemporarilyUnavailable, } pub enum OIDCErrorPayload<'a> { Borrowed(&'a str), Owned(String), } impl<'a> OIDCErrorPayload<'a> { fn as_str(&self) -> &str { match self { Self::Borrowed(s) => s, Self::Owned(s) => s.as_str(), } } } impl<'a> From<&'a str> for OIDCErrorPayload<'a> { fn from(value: &'a str) -> Self { Self::Borrowed(value) } } impl<'a> From for OIDCErrorPayload<'a> { fn from(value: String) -> Self { Self::Owned(value) } } /// error type, pub struct OIDCError<'a>(OIDCErrorType, OIDCErrorPayload<'a>, Option<&'a str>); impl<'a> OIDCError<'a> { fn into_response(self) -> tide::Response { #[derive(Serialize)] struct ErrorOut<'a> { error: OIDCErrorType, error_description: &'a str, state: Option<&'a str>, } let eo = ErrorOut { error: self.0, error_description: self.1.as_str(), state: self.2, }; tide::Response::builder(400) .body(serde_json::to_vec(&eo).unwrap()) .build() } } impl<'a> From for OIDCError<'a> { fn from(value: microrm::Error) -> Self { Self( OIDCErrorType::ServerError, format!("Internal database error: {value}").into(), None, ) } } async fn authorize(request: Request) -> tide::Result { #[derive(Deserialize)] struct State { state: Option, } let state: Option = request.query::().ok().and_then(|x| x.state); match authorize::do_authorize(request, state.as_deref()) { Ok(r) => Ok(r), Err(e) => Ok(e.into_response()), } } async fn token(request: Request) -> tide::Result { match token::do_token(request).await { Ok(res) => Ok(res), Err(e) => Ok(e.into_response()), } } async fn jwks(request: Request) -> tide::Result { let shelper = SessionHelper::new(&request); let realm = shelper.get_realm()?; // build JWK set let mut jwkset = jsonwebtoken::jwk::JwkSet { keys: vec![] }; for key in realm.keys.get()?.into_iter() { if *key.key_state.as_ref() == schema::KeyState::Retired { continue; } // skip HMAC keys if let key::KeyType::HMac(_) = *key.key_type.as_ref() { continue; } jwkset.keys.push(key.wrapped().into_jwk()); } Ok(tide::Response::builder(200) .header(tide::http::headers::ACCESS_CONTROL_ALLOW_ORIGIN, "*") .content_type(tide::http::mime::JSON) .body(serde_json::to_vec(&jwkset).unwrap()) .build()) } async fn discovery_config(request: Request) -> tide::Result { let server_config = &request.state().core.config; let base_url = format!( "{}/{}", server_config.base_url, request.param("realm").unwrap() ); let config_response = serde_json::json!({ "issuer": base_url, "authorization_endpoint": format!("{}/{}", base_url, AUTHORIZE_PATH), "token_endpoint": format!("{}/{}", base_url, TOKEN_PATH), "jwks_uri": format!("{}/{}", base_url, JWKS_PATH), "token_endpoint_auth_signing_alg_values_supported": ["EdDSA", "RS256"], "response_types_supported": ["code", "id_token", "token id_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["EdDSA", "RS256"], }); Ok(tide::Response::builder(200) .header(tide::http::headers::ACCESS_CONTROL_ALLOW_ORIGIN, "*") .body(config_response) .build()) } pub(super) fn oidc_server(mut route: tide::Route) { route.at(AUTHORIZE_PATH).get(authorize).post(authorize); route.at(TOKEN_PATH).post(token); route.at(JWKS_PATH).get(jwks); route.at(DISCOVERY_PATH).get(discovery_config); }