Skip to main content

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 secrecy::ExposeSecret;
9use sha2::{Digest as ShaDigest, Sha256};
10
11use crate::domain::error::ControllerError;
12use crate::domain::oauth::claims::Claims;
13use crate::domain::oauth::helpers::oauth_server_error;
14use crate::prelude::ApplicationConfiguration;
15
16pub fn rsa_n_e_and_kid_from_pem(public_pem: &str) -> anyhow::Result<(String, String, String)> {
17    let pubkey = match RsaPublicKey::from_pkcs1_pem(public_pem) {
18        Ok(k) => k,
19        Err(_) => RsaPublicKey::from_public_key_pem(public_pem)?,
20    };
21
22    let n_b64 = URL_SAFE_NO_PAD.encode(pubkey.n().to_bytes_be());
23    let e_b64 = URL_SAFE_NO_PAD.encode(pubkey.e().to_bytes_be());
24
25    let spki_der = pubkey.to_public_key_der()?;
26    let kid = URL_SAFE_NO_PAD.encode(Sha256::digest(spki_der.as_bytes()));
27
28    Ok((n_b64, e_b64, kid))
29}
30
31/// Generate an ID token. `nonce` should be `Some` only when the authorization request
32/// included a nonce; when absent or empty, the nonce claim is omitted from the id_token.
33pub fn generate_id_token(
34    user_id: uuid::Uuid,
35    client_id: &str,
36    nonce: Option<&str>,
37    expires_at: DateTime<Utc>,
38    issuer: &str,
39    cfg: &ApplicationConfiguration,
40) -> Result<String, ControllerError> {
41    let now = Utc::now().timestamp();
42    let exp = expires_at.timestamp();
43
44    let (_, _, kid) = rsa_n_e_and_kid_from_pem(&cfg.oauth_server_configuration.rsa_public_key)
45        .map_err(|e| oauth_server_error("Failed to derive key id (kid) from public key", e))?;
46
47    let nonce_claim = nonce.and_then(|s| {
48        if s.is_empty() {
49            None
50        } else {
51            Some(s.to_string())
52        }
53    });
54
55    let claims = Claims {
56        sub: user_id.to_string(),
57        aud: client_id.to_string(),
58        iss: issuer.to_string(),
59        iat: now,
60        exp,
61        nonce: nonce_claim,
62    };
63
64    let mut header = Header::new(jsonwebtoken::Algorithm::RS256);
65    header.kid = Some(kid);
66
67    let enc_key = EncodingKey::from_rsa_pem(
68        cfg.oauth_server_configuration
69            .rsa_private_key
70            .expose_secret()
71            .as_bytes(),
72    )
73    .map_err(|e| oauth_server_error("Failed to generate ID token", e.into()))?;
74
75    encode(&header, &claims, &enc_key)
76        .map_err(|e| oauth_server_error("Failed to generate ID token", e.into()))
77}