headless_lms_server/domain/oauth/
oidc.rs

1use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
2use chrono::{DateTime, Utc};
3use jsonwebtoken::{EncodingKey, Header, encode};
4use rsa::RsaPublicKey;
5use rsa::pkcs1::DecodeRsaPublicKey;
6use rsa::pkcs8::{DecodePublicKey, EncodePublicKey};
7use rsa::traits::PublicKeyParts;
8use sha2::{Digest as ShaDigest, Sha256};
9
10use crate::domain::error::{ControllerError, ControllerErrorType, OAuthErrorCode, OAuthErrorData};
11use crate::domain::oauth::claims::Claims;
12use crate::prelude::{ApplicationConfiguration, BackendError};
13
14pub fn rsa_n_e_and_kid_from_pem(public_pem: &str) -> anyhow::Result<(String, String, String)> {
15    let pubkey = match RsaPublicKey::from_pkcs1_pem(public_pem) {
16        Ok(k) => k,
17        Err(_) => RsaPublicKey::from_public_key_pem(public_pem)?,
18    };
19
20    let n_b64 = URL_SAFE_NO_PAD.encode(pubkey.n().to_bytes_be());
21    let e_b64 = URL_SAFE_NO_PAD.encode(pubkey.e().to_bytes_be());
22
23    let spki_der = pubkey.to_public_key_der()?;
24    let kid = URL_SAFE_NO_PAD.encode(Sha256::digest(spki_der.as_bytes()));
25
26    Ok((n_b64, e_b64, kid))
27}
28
29/// Generate an ID token. `nonce` should be `Some` only when the authorization request
30/// included a nonce; when absent or empty, the nonce claim is omitted from the id_token.
31pub fn generate_id_token(
32    user_id: uuid::Uuid,
33    client_id: &str,
34    nonce: Option<&str>,
35    expires_at: DateTime<Utc>,
36    issuer: &str,
37    cfg: &ApplicationConfiguration,
38) -> Result<String, ControllerError> {
39    let now = Utc::now().timestamp();
40    let exp = expires_at.timestamp();
41
42    let (_, _, kid) = rsa_n_e_and_kid_from_pem(&cfg.oauth_server_configuration.rsa_public_key)
43        .map_err(|e| {
44            ControllerError::new(
45                ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
46                    error: OAuthErrorCode::ServerError.as_str().into(),
47                    error_description: "Failed to derive key id (kid) from public key".into(),
48                    redirect_uri: None,
49                    state: None,
50                    nonce: None,
51                })),
52                "Failed to derive kid from public key",
53                Some(e),
54            )
55        })?;
56
57    let nonce_claim = nonce.and_then(|s| {
58        if s.is_empty() {
59            None
60        } else {
61            Some(s.to_string())
62        }
63    });
64
65    let claims = Claims {
66        sub: user_id.to_string(),
67        aud: client_id.to_string(),
68        iss: issuer.to_string(),
69        iat: now,
70        exp,
71        nonce: nonce_claim,
72    };
73
74    let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
75    header.kid = Some(kid);
76
77    let enc_key =
78        EncodingKey::from_rsa_pem(cfg.oauth_server_configuration.rsa_private_key.as_bytes())
79            .map_err(|e| {
80                ControllerError::new(
81                    ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
82                        error: OAuthErrorCode::ServerError.as_str().into(),
83                        error_description: "Failed to generate ID token".into(),
84                        redirect_uri: None,
85                        state: None,
86                        nonce: None,
87                    })),
88                    "Failed to generate ID token (invalid private key)",
89                    Some(e.into()),
90                )
91            })?;
92
93    encode(&header, &claims, &enc_key).map_err(|e| {
94        ControllerError::new(
95            ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
96                error: OAuthErrorCode::ServerError.as_str().into(),
97                error_description: "Failed to generate ID token".into(),
98                redirect_uri: None,
99                state: None,
100                nonce: None,
101            })),
102            "Failed to generate ID token",
103            Some(e.into()),
104        )
105    })
106}