Browse Source

Serve the login JWKS via OIDC discovery

Services that verify our access tokens, like a key connector, had to
get a copy of the RSA public key by hand and keep it in sync. Publish
issuer and signing key at /identity/.well-known/openid-configuration
instead, the same way the Bitwarden identity service shares them with
its consumers.
pull/7419/head
Luca Biemelt 2 weeks ago
parent
commit
826aea858e
  1. 19
      src/api/identity.rs
  2. 28
      src/auth.rs

19
src/api/identity.rs

@ -52,10 +52,27 @@ pub fn routes() -> Vec<Route> {
prevalidate,
authorize,
oidcsignin,
oidcsignin_error
oidcsignin_error,
openid_configuration,
jwks
]
}
// Issuer and signing key of our own login tokens, for services that verify
// them (e.g. a key connector). The issuer is the `iss` claim, not a URL.
#[get("/.well-known/openid-configuration")]
fn openid_configuration() -> Json<Value> {
Json(json!({
"issuer": *auth::JWT_LOGIN_ISSUER,
"jwks_uri": format!("{}/identity/.well-known/jwks", CONFIG.domain()),
}))
}
#[get("/.well-known/jwks")]
fn jwks() -> Json<Value> {
Json(auth::login_jwks().clone())
}
#[post("/connect/token", data = "<data>")]
async fn login(
data: Form<ConnectData>,

28
src/auth.rs

@ -10,10 +10,12 @@ use std::{
};
use chrono::{DateTime, TimeDelta, Utc};
use data_encoding::BASE64URL_NOPAD;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, errors::ErrorKind};
use num_traits::FromPrimitive;
use openssl::rsa::Rsa;
use serde::{de::DeserializeOwned, ser::Serialize};
use serde_json::json;
use rocket::{
outcome::try_outcome,
@ -64,6 +66,7 @@ static JWT_2FA_REMEMBER_ISSUER: LazyLock<String> = LazyLock::new(|| format!("{}|
static PRIVATE_RSA_KEY: OnceLock<EncodingKey> = OnceLock::new();
static PUBLIC_RSA_KEY: OnceLock<DecodingKey> = OnceLock::new();
static LOGIN_JWKS: OnceLock<serde_json::Value> = OnceLock::new();
pub async fn initialize_keys() -> Result<(), Error> {
use std::io::Error as IoError;
@ -90,6 +93,24 @@ pub async fn initialize_keys() -> Result<(), Error> {
};
let pub_key_buffer = priv_key.public_key_to_pem()?;
// Expose the public half as a JWKS on /identity/.well-known/ so token
// consumers (e.g. a key connector) don't need a copy of the PEM.
let n = BASE64URL_NOPAD.encode(&priv_key.n().to_vec());
let e = BASE64URL_NOPAD.encode(&priv_key.e().to_vec());
// the kid is the RFC 7638 thumbprint of the key
let thumbprint = json!({"e": e, "kty": "RSA", "n": n}).to_string();
let kid = BASE64URL_NOPAD.encode(&openssl::sha::sha256(thumbprint.as_bytes()));
let jwks = json!({
"keys": [{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": kid,
"n": n,
"e": e,
}]
});
let enc = EncodingKey::from_rsa_pem(&priv_key_buffer)?;
let dec: DecodingKey = DecodingKey::from_rsa_pem(&pub_key_buffer)?;
if PRIVATE_RSA_KEY.set(enc).is_err() {
@ -98,9 +119,16 @@ pub async fn initialize_keys() -> Result<(), Error> {
if PUBLIC_RSA_KEY.set(dec).is_err() {
err!("PUBLIC_RSA_KEY must only be initialized once")
}
if LOGIN_JWKS.set(jwks).is_err() {
err!("LOGIN_JWKS must only be initialized once")
}
Ok(())
}
pub fn login_jwks() -> &'static serde_json::Value {
LOGIN_JWKS.wait()
}
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
match jsonwebtoken::encode(&JWT_HEADER, claims, PRIVATE_RSA_KEY.wait()) {
Ok(token) => token,

Loading…
Cancel
Save