Skip to content

Commit 5bd3ac8

Browse files
committed
feat: Add support for custom claims in OIDC JWT tokens
This enhancement allows SQLPage to read and utilize custom claims from JWT tokens generated by OIDC providers, enabling users to store additional information like roles or permissions that can be used in SQL queries. Key changes: - Modified get_token_claims() to accept optional login state, allowing token verification without requiring active login session state - Updated nonce verification logic to handle cases where no login state exists - Refactored OIDC client and token response types to use more specific generics that support additional claims through OidcAdditionalClaims - Simplified function signatures by passing oidc_state instead of separate client and config parameters - Enhanced error handling in OIDC callback processing with automatic client refresh - Updated type definitions to properly support custom claim extraction from tokens This enables SSO providers to include custom user metadata that SQLPage can access and use for authorization and personalization in database queries.
1 parent df080ec commit 5bd3ac8

File tree

2 files changed

+68
-44
lines changed

2 files changed

+68
-44
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
```
5050
- The file-based routing system was improved. Now, requests to `/xxx` redirect to `/xxx/` only if `/xxx/index.sql` exists.
5151
- fix: When single sign on is enabled, and an anonymous user visits a page with URL parameters, the user is correctly redirected to the page with the parameters after login.
52+
- Added support for reading custom claims in JWT tokens generated by OIDC providers. This means that you can configure your Single-Sign-On provider to store custom pieces of information about your users, such as roles or permissions, and use them in your SQL queries in SQLPage.
5253

5354
## v0.35.2
5455
- Fix a bug with zero values being displayed with a non-zero height in stacked bar charts.

src/webserver/oidc.rs

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ use actix_web::{
1717
use anyhow::{anyhow, Context};
1818
use awc::Client;
1919
use chrono::Utc;
20-
use openidconnect::core::CoreJsonWebKey;
21-
use openidconnect::IdTokenVerifier;
20+
use openidconnect::core::{
21+
CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey,
22+
CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreRevocableToken,
23+
CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType,
24+
};
2225
use openidconnect::{
2326
core::CoreAuthenticationFlow, url::Url, AsyncHttpClient, Audience, CsrfToken, EndpointMaybeSet,
2427
EndpointNotSet, EndpointSet, IssuerUrl, Nonce, OAuth2TokenResponse, RedirectUrl, Scope,
2528
TokenResponse,
2629
};
30+
use openidconnect::{
31+
EmptyExtraTokenFields, IdTokenFields, IdTokenVerifier, StandardErrorResponse,
32+
StandardTokenResponse,
33+
};
2734
use serde::{Deserialize, Serialize};
2835
use std::sync::Mutex;
2936

@@ -197,12 +204,12 @@ impl OidcState {
197204
fn get_token_claims(
198205
&self,
199206
id_token: &OidcToken,
200-
state: &OidcLoginState,
207+
state: Option<&OidcLoginState>,
201208
) -> anyhow::Result<OidcClaims> {
202209
// Do not refresh the client on every check
203210
let client = &self.client.lock().expect("oidc client").client;
204211
let verifier = self.config.create_id_token_verifier(client);
205-
let nonce_verifier = |nonce: Option<&Nonce>| check_nonce(nonce, &state.nonce);
212+
let nonce_verifier = |nonce: Option<&Nonce>| check_nonce(nonce, state);
206213
let claims: OidcClaims = id_token
207214
.claims(&verifier, nonce_verifier)
208215
.with_context(|| format!("Could not verify the ID token: {id_token:?}"))?
@@ -321,11 +328,7 @@ where
321328

322329
let oidc_state = Arc::clone(&self.oidc_state);
323330
Box::pin(async move {
324-
let response = build_auth_provider_redirect_response(
325-
&oidc_state.get_client().await.client,
326-
&oidc_state.config,
327-
&request,
328-
);
331+
let response = build_auth_provider_redirect_response(&oidc_state, &request);
329332
Ok(request.into_response(response))
330333
})
331334
}
@@ -338,18 +341,12 @@ where
338341

339342
Box::pin(async move {
340343
let query_string = request.query_string();
341-
let client = oidc_state.get_client().await;
342-
match process_oidc_callback(&client.client, &oidc_state.config, query_string, &request)
343-
.await
344-
{
344+
match process_oidc_callback(&oidc_state, query_string, &request).await {
345345
Ok(response) => Ok(request.into_response(response)),
346346
Err(e) => {
347347
log::error!("Failed to process OIDC callback with params {query_string}: {e}");
348-
let resp = build_auth_provider_redirect_response(
349-
&client.client,
350-
&oidc_state.config,
351-
&request,
352-
);
348+
oidc_state.refresh().await;
349+
let resp = build_auth_provider_redirect_response(&oidc_state, &request);
353350
Ok(request.into_response(resp))
354351
}
355352
}
@@ -412,8 +409,7 @@ where
412409
}
413410

414411
async fn process_oidc_callback(
415-
oidc_client: &OidcClient,
416-
oidc_config: &OidcConfig,
412+
oidc_state: &OidcState,
417413
query_string: &str,
418414
request: &ServiceRequest,
419415
) -> anyhow::Result<HttpResponse> {
@@ -434,22 +430,23 @@ async fn process_oidc_callback(
434430
return Err(anyhow!("Invalid CSRF token: {}", params.state.secret()));
435431
}
436432

433+
let client = oidc_state.get_client().await;
437434
log::debug!("Processing OIDC callback with params: {params:?}. Requesting token...");
438-
let token_response = exchange_code_for_token(oidc_client, http_client, params).await?;
435+
let token_response = exchange_code_for_token(&client.client, http_client, params).await?;
439436
log::debug!("Received token response: {token_response:?}");
440437

441438
let redirect_target = validate_redirect_url(state.initial_url);
442439
log::info!("Redirecting to {redirect_target} after a successful login");
443440
let mut response = build_redirect_response(redirect_target);
444-
set_auth_cookie(&mut response, &token_response, oidc_client, oidc_config)?;
441+
set_auth_cookie(&mut response, &token_response, oidc_state)?;
445442
Ok(response)
446443
}
447444

448445
async fn exchange_code_for_token(
449446
oidc_client: &OidcClient,
450447
http_client: &awc::Client,
451448
oidc_callback_params: OidcCallbackParams,
452-
) -> anyhow::Result<openidconnect::core::CoreTokenResponse> {
449+
) -> anyhow::Result<OidcTokenResponse> {
453450
let token_response = oidc_client
454451
.exchange_code(openidconnect::AuthorizationCode::new(
455452
oidc_callback_params.code,
@@ -461,19 +458,16 @@ async fn exchange_code_for_token(
461458

462459
fn set_auth_cookie(
463460
response: &mut HttpResponse,
464-
token_response: &openidconnect::core::CoreTokenResponse,
465-
oidc_client: &OidcClient,
466-
oidc_config: &OidcConfig,
461+
token_response: &OidcTokenResponse,
462+
oidc_state: &OidcState,
467463
) -> anyhow::Result<()> {
468464
let access_token = token_response.access_token();
469465
log::trace!("Received access token: {}", access_token.secret());
470466
let id_token = token_response
471467
.id_token()
472468
.context("No ID token found in the token response. You may have specified an oauth2 provider that does not support OIDC.")?;
473469

474-
let id_token_verifier = oidc_config.create_id_token_verifier(oidc_client);
475-
let nonce_verifier = |_nonce: Option<&Nonce>| Ok(()); // The nonce will be verified in request handling
476-
let claims = id_token.claims(&id_token_verifier, nonce_verifier)?;
470+
let claims = oidc_state.get_token_claims(id_token, None)?;
477471
let expiration = claims.expiration();
478472
let max_age_seconds = expiration.signed_duration_since(Utc::now()).num_seconds();
479473

@@ -499,11 +493,10 @@ fn set_auth_cookie(
499493
}
500494

501495
fn build_auth_provider_redirect_response(
502-
oidc_client: &OidcClient,
503-
oidc_config: &OidcConfig,
496+
oidc_state: &OidcState,
504497
request: &ServiceRequest,
505498
) -> HttpResponse {
506-
let AuthUrl { url, params } = build_auth_url(oidc_client, &oidc_config.scopes);
499+
let AuthUrl { url, params } = build_auth_url(oidc_state);
507500
let state_cookie = create_state_cookie(request, params);
508501
HttpResponse::TemporaryRedirect()
509502
.append_header(("Location", url.to_string()))
@@ -530,7 +523,7 @@ fn get_authenticated_user_info(
530523
.with_context(|| format!("Invalid SQLPage auth cookie: {cookie_value:?}"))?;
531524

532525
let state = get_state_from_cookie(request)?;
533-
let claims = oidc_state.get_token_claims(&id_token, &state)?;
526+
let claims = oidc_state.get_token_claims(&id_token, Some(&state))?;
534527
log::debug!("The current user is: {claims:?}");
535528
Ok(Some(claims))
536529
}
@@ -606,14 +599,38 @@ impl std::fmt::Display for AwcWrapperError {
606599
std::fmt::Display::fmt(&self.0, f)
607600
}
608601
}
609-
type OidcClient = openidconnect::core::CoreClient<
602+
603+
type OidcTokenResponse = StandardTokenResponse<
604+
IdTokenFields<
605+
OidcAdditionalClaims,
606+
EmptyExtraTokenFields,
607+
CoreGenderClaim,
608+
CoreJweContentEncryptionAlgorithm,
609+
CoreJwsSigningAlgorithm,
610+
>,
611+
CoreTokenType,
612+
>;
613+
614+
type OidcClient = openidconnect::Client<
615+
OidcAdditionalClaims,
616+
CoreAuthDisplay,
617+
CoreGenderClaim,
618+
CoreJweContentEncryptionAlgorithm,
619+
CoreJsonWebKey,
620+
CoreAuthPrompt,
621+
StandardErrorResponse<CoreErrorResponseType>,
622+
OidcTokenResponse,
623+
CoreTokenIntrospectionResponse,
624+
CoreRevocableToken,
625+
CoreRevocationErrorResponse,
610626
EndpointSet,
611627
EndpointNotSet,
612628
EndpointNotSet,
613629
EndpointNotSet,
614630
EndpointMaybeSet,
615631
EndpointMaybeSet,
616632
>;
633+
617634
impl std::error::Error for AwcWrapperError {
618635
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
619636
self.0.source()
@@ -650,12 +667,9 @@ fn make_oidc_client(
650667
))?;
651668
}
652669
log::info!("OIDC redirect URL for {}: {redirect_url}", config.client_id);
653-
let client = openidconnect::core::CoreClient::from_provider_metadata(
654-
provider_metadata,
655-
client_id,
656-
Some(client_secret),
657-
)
658-
.set_redirect_uri(redirect_url);
670+
let client =
671+
OidcClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret))
672+
.set_redirect_uri(redirect_url);
659673

660674
Ok(client)
661675
}
@@ -676,10 +690,13 @@ struct AuthUrlParams {
676690
nonce: Nonce,
677691
}
678692

679-
fn build_auth_url(oidc_client: &OidcClient, scopes: &[Scope]) -> AuthUrl {
693+
fn build_auth_url(oidc_state: &OidcState) -> AuthUrl {
680694
let nonce_source = Nonce::new_random();
681695
let hashed_nonce = Nonce::new(hash_nonce(&nonce_source));
682-
let (url, csrf_token, _nonce) = oidc_client
696+
let scopes = &oidc_state.config.scopes;
697+
let client_lock = oidc_state.client.lock().unwrap();
698+
let (url, csrf_token, _nonce) = client_lock
699+
.client
683700
.authorize_url(
684701
CoreAuthenticationFlow::AuthorizationCode,
685702
CsrfToken::new_random,
@@ -722,9 +739,15 @@ fn hash_nonce(nonce: &Nonce) -> String {
722739
hash.to_string()
723740
}
724741

725-
fn check_nonce(id_token_nonce: Option<&Nonce>, state_nonce: &Nonce) -> Result<(), String> {
742+
fn check_nonce(
743+
id_token_nonce: Option<&Nonce>,
744+
login_state: Option<&OidcLoginState>,
745+
) -> Result<(), String> {
746+
let Some(state) = login_state else {
747+
return Ok(()); // No login state, no nonce to check
748+
};
726749
match id_token_nonce {
727-
Some(id_token_nonce) => nonce_matches(id_token_nonce, state_nonce),
750+
Some(id_token_nonce) => nonce_matches(id_token_nonce, &state.nonce),
728751
None => Err("No nonce found in the ID token".to_string()),
729752
}
730753
}

0 commit comments

Comments
 (0)