OIDC: Extract username from JWT token

This commit is contained in:
Maurus Decimus
2026-04-24 15:20:37 +02:00
parent 2458e6c3d8
commit 96795b0cf2
3 changed files with 39 additions and 4 deletions

View File

@@ -12,6 +12,7 @@ use crate::{
oauth::GrantType,
},
};
use base64::{Engine, engine::general_purpose};
use directory::{
Credentials, Directory,
core::secret::{SecretVerificationResult, verify_mfa_secret_hash, verify_secret_hash},
@@ -297,14 +298,20 @@ impl Server {
return Ok(AccessToken::new_admin());
}
// Obtain external directory, if any
// Obtain external directory, if any. When no username is supplied
// (e.g. HTTP bearer auth), peek at the JWT claims to find the
// user's domain so per-domain OIDC directories are reachable.
let directory = if let Some(username) = username.as_deref().map(UsernameParts::new)
{
if let Some(domain_name) = username.auth_as().domain() {
self.get_directory_for_domain(domain_name).await?
} else if let Some(domain_name) = extract_jwt_domain(token) {
self.get_directory_for_domain(&domain_name).await?
} else {
self.get_default_directory()
}
} else if let Some(domain_name) = extract_jwt_domain(token) {
self.get_directory_for_domain(&domain_name).await?
} else {
self.get_default_directory()
};
@@ -496,6 +503,27 @@ impl Server {
}
}
fn extract_jwt_domain(token: &str) -> Option<String> {
let mut parts = token.split('.');
let _header = parts.next()?;
let payload = parts.next()?;
let _signature = parts.next()?;
if parts.next().is_some() {
return None;
}
let payload_bytes = general_purpose::URL_SAFE_NO_PAD.decode(payload).ok()?;
let claims: serde_json::Value = serde_json::from_slice(&payload_bytes).ok()?;
for claim in ["email", "preferred_username", "upn"] {
if let Some(val) = claims.get(claim).and_then(|v| v.as_str())
&& let Some((_, domain)) = val.rsplit_once('@')
&& !domain.is_empty()
{
return Some(domain.to_ascii_lowercase());
}
}
None
}
impl UsernameParts {
pub fn new(address: &str) -> Self {
let mut account = Username {

View File

@@ -16,6 +16,12 @@ use store::{
use trc::AddContext;
use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
pub const FAILED_TO_DECODE_TOKEN: &str = concat!(
"Failed to decode token. If you are using an ",
"external OIDC provider, make sure it is configured as the default directory under ",
"the Authentication object."
);
pub struct TokenInfo {
pub grant_type: GrantType,
pub account_id: u32,
@@ -115,7 +121,7 @@ impl Server {
.map_err(|_| {
trc::AuthEvent::Error
.into_err()
.ctx(trc::Key::Reason, "Failed to decode token")
.ctx(trc::Key::Reason, FAILED_TO_DECODE_TOKEN)
.caused_by(trc::location!())
.details(token_.to_string())
})?;
@@ -135,7 +141,7 @@ impl Server {
.ok_or_else(|| {
trc::AuthEvent::Error
.into_err()
.ctx(trc::Key::Reason, "Failed to decode token")
.ctx(trc::Key::Reason, FAILED_TO_DECODE_TOKEN)
.caused_by(trc::location!())
.details(token_.to_string())
})?;
@@ -200,7 +206,7 @@ impl Server {
.map_err(|err| {
trc::AuthEvent::Error
.into_err()
.ctx(trc::Key::Details, "Failed to decode token")
.ctx(trc::Key::Details, FAILED_TO_DECODE_TOKEN)
.caused_by(trc::location!())
.reason(err)
})?;