rustical_oidc/
config.rs

1use crate::error::OidcError;
2use openidconnect::{
3    AdditionalClaims, Audience, ClientId, ClientSecret, GenderClaim, IssuerUrl, Scope,
4    UserInfoClaims,
5};
6use serde::{Deserialize, Deserializer, Serialize};
7use std::collections::HashMap;
8
9#[derive(Deserialize, Serialize, Clone, Default)]
10#[serde(rename_all = "snake_case")]
11pub enum UserIdClaim {
12    // The correct option
13    Sub,
14    // The more ergonomic option if you know what you're doing
15    #[default]
16    PreferredUsername,
17    // The hopefully unique option
18    Email,
19}
20
21impl UserIdClaim {
22    pub fn extract_user_id<AC: AdditionalClaims, GC: GenderClaim>(
23        &self,
24        claims: &UserInfoClaims<AC, GC>,
25    ) -> Result<String, OidcError> {
26        Ok(match self {
27            Self::Sub => claims.subject().to_string(),
28            Self::PreferredUsername => claims
29                .preferred_username()
30                .ok_or(OidcError::Other("Missing preferred_username claim"))?
31                .to_string(),
32            Self::Email => claims
33                .email()
34                .ok_or(OidcError::Other("Missing email claim"))?
35                .to_string(),
36        })
37    }
38}
39
40/// Deserialize a `ClientId` from either string or integer input.
41/// Identity providers like Zitadel generate numeric client ids which would otherwise need to be quoted as `RUSTICAL_OIDC__CLIENT_ID="\"123\""` which may be counterintuitive
42fn deserialize_client_id<'de, D>(deserializer: D) -> Result<ClientId, D::Error>
43where
44    D: Deserializer<'de>,
45{
46    #[derive(Deserialize)]
47    #[serde(untagged)]
48    enum StringOrInt {
49        String(String),
50        Integer(i64),
51    }
52
53    let string_val = match StringOrInt::deserialize(deserializer)? {
54        StringOrInt::String(s) => s,
55        StringOrInt::Integer(i) => i.to_string(),
56    };
57
58    Ok(ClientId::new(string_val))
59}
60
61#[derive(Deserialize, Serialize, Clone)]
62#[serde(deny_unknown_fields)]
63pub struct OidcConfig {
64    pub name: String,
65    pub issuer: IssuerUrl,
66    #[serde(deserialize_with = "deserialize_client_id")]
67    pub client_id: ClientId,
68    pub client_secret: Option<ClientSecret>,
69    pub scopes: Vec<Scope>,
70    #[serde(default)]
71    pub allow_sign_up: bool,
72    pub require_group: Option<String>,
73    #[serde(default)]
74    pub claim_userid: UserIdClaim,
75    #[serde(default)]
76    pub additional_audiences: Vec<Audience>,
77    #[serde(default)]
78    pub assign_memberships: HashMap<String, Vec<String>>,
79}