feat: support OIDC id_token

- generate JWT id_token in token exchange
- store optional nonce in authorization object
- switch to RS256 algorithm for JWT signature
- add JWKs endpoint to provide OIDC clients with public keys
This commit is contained in:
Matthieu Bessat 2024-12-12 01:12:40 +01:00
parent ca84a0f99f
commit d982f2af0d
29 changed files with 255 additions and 82 deletions

View file

@ -7,9 +7,7 @@ use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use kernel::{
models::{authorization::Authorization, config::AppAuthorizeFlow}
};
use kernel::models::{authorization::Authorization, config::AppAuthorizeFlow};
use utils::get_random_alphanumerical;
use crate::{
renderer::TemplateRenderer, services::oauth2::{parse_scope, verify_redirect_uri}, token_claims::UserTokenClaims, AppState
@ -25,6 +23,7 @@ struct AuthorizationParams {
redirect_uri: String,
/// An opaque value used by the client to maintain state between the request and callback
state: String,
nonce: Option<String>
}
fn redirect_to_client(
@ -34,7 +33,7 @@ fn redirect_to_client(
let target_url = format!("{}?code={}&state={}",
authorization_params.redirect_uri,
authorization_code,
authorization_params.state,
authorization_params.state
);
debug!("Redirecting to {}", target_url);
@ -117,9 +116,10 @@ pub async fn authorize_form(
// Create new auth code
let authorization_code = get_random_alphanumerical(32);
// Update last used timestamp for this authorization
let _result = sqlx::query("UPDATE authorizations SET code = $2, last_used_at = $3 WHERE id = $1")
let _result = sqlx::query("UPDATE authorizations SET code = $2, nonce = $3, last_used_at = $4 WHERE id = $1")
.bind(existing_authorization.id)
.bind(authorization_code.clone())
.bind(authorization_params.nonce.clone())
.bind(Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true))
.execute(&app_state.db.0)
.await.unwrap();
@ -173,6 +173,7 @@ pub async fn perform_authorize(
Extension(token_claims): Extension<UserTokenClaims>,
Form(authorize_form): Form<AuthorizationParams>
) -> impl IntoResponse {
dbg!(&authorize_form);
// 1. Get the app details
let app = match app_state.config.applications
.iter()
@ -204,6 +205,7 @@ pub async fn perform_authorize(
client_id: app.client_id.clone(),
scopes: sqlx::types::Json(scopes),
code: authorization_code.clone(),
nonce: authorize_form.nonce.clone(),
last_used_at: Some(Utc::now()),
created_at: Utc::now(),
};
@ -211,14 +213,15 @@ pub async fn perform_authorize(
// 3. Save authorization in DB with state
let res = sqlx::query("
INSERT INTO authorizations
(id, user_id, client_id, scopes, code, last_used_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
(id, user_id, client_id, scopes, code, nonce, last_used_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
")
.bind(authorization.id.clone())
.bind(authorization.user_id)
.bind(authorization.client_id)
.bind(authorization.scopes)
.bind(authorization.code)
.bind(authorization.nonce)
.bind(authorization.last_used_at.map(|x| x.to_rfc3339_opts(SecondsFormat::Millis, true)))
.bind(authorization.created_at.to_rfc3339_opts(SecondsFormat::Millis, true))
.execute(&app_state.db.0)