Skip to content

Adding support for jwt token authorization authentication with a central Jwks - #131

Open
dawid-nowak wants to merge 7 commits into
mainfrom
dawid.nowak/adding_jwt_token_authorization_authentication
Open

Adding support for jwt token authorization authentication with a central Jwks#131
dawid-nowak wants to merge 7 commits into
mainfrom
dawid.nowak/adding_jwt_token_authorization_authentication

Conversation

@dawid-nowak

Copy link
Copy Markdown
Contributor
  • Enhanced support for AutzN/Z
  • The verification keys are retrieved from centralized JWKS which needs to be configured at startup
  • Isolation of claims and AuthorizedPrincipal. AuthorizedPrincipal is created based on the claims from the JWT token but it doesn't need to be. It should be used to isolate higher layers from the knowledge about the claims.

Signed-off-by: Dawid Nowak <nowakd@gmail.com>
Signed-off-by: Dawid Nowak <nowakd@gmail.com>
Signed-off-by: Dawid Nowak <nowakd@gmail.com>
Signed-off-by: Dawid Nowak <nowakd@gmail.com>
Signed-off-by: Dawid Nowak <nowakd@gmail.com>
Signed-off-by: Dawid Nowak <nowakd@gmail.com>

@lucarlig lucarlig left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observed review and E2E outcomes:

  • The expired-token test rejected before expiry validation because no verification keys were configured.
  • The dataplane exited during startup because the E2E inputs did not provide the required JWKS/private-key configuration, so both external lanes remained at HTTP 502 and no MCP scenarios ran.
  • Key selection and SaaS principal extraction do not preserve the required constraints and mappings.

Comment thread crates/contextforge-data-plane-lib/src/common.rs
Comment thread crates/contextforge-data-plane-lib/src/authorization/jwks/jwks_authorization.rs Outdated
Comment thread crates/contextforge-data-plane-lib/src/authorization/jwks/jwks.rs
Signed-off-by: Dawid Nowak <nowakd@gmail.com>
@lucarlig

lucarlig commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

/conformance

@lucarlig lucarlig left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concrete options for the open threads. The conformance topology would still need the self-hosted JWKS URL, the extractor would need a public re-export and injected constructor, and the HMAC fixtures would need RSA replacements if the algorithm policy is adopted.

pub mcp_allowed_hosts: Option<Vec<Authority>>,

#[cfg(feature = "with_tools")]
#[arg(long)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would restoring the environment-backed input work here?

Suggested change
#[arg(long)]
#[arg(long, env = "CONTEXTFORGE_DATA_PLANE_TOKEN_VERIFICATION_PRIVATE_KEY")]

Comment on lines +39 to +42
async fn authorize_token(&self, token: &str) -> Option<AuthorizationClaims> {
let header = decode_header(token).ok()?;
self.jwks.validate(token, &header).await
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this make the accepted header contract explicit?

Suggested change
async fn authorize_token(&self, token: &str) -> Option<AuthorizationClaims> {
let header = decode_header(token).ok()?;
self.jwks.validate(token, &header).await
}
async fn authorize_token(&self, token: &str) -> Option<AuthorizationClaims> {
let header = decode_header(token).ok()?;
header.kid.as_deref()?;
matches!(
header.alg,
jsonwebtoken::Algorithm::RS256
| jsonwebtoken::Algorithm::RS384
| jsonwebtoken::Algorithm::RS512
| jsonwebtoken::Algorithm::ES256
| jsonwebtoken::Algorithm::ES384
)
.then_some(())?;
self.jwks.validate(token, &header).await
}

Comment on lines +112 to +140
pub struct VerificationKey {
pub(crate) key_id: Option<String>,
pub(crate) decoding_key: DecodingKey,
}

impl VerificationKey {
fn from_jwk(jwk: Jwk) -> Result<Option<Self>, AuthorizationError> {
if jwk.common.public_key_use.as_ref().is_some_and(|key_use| key_use != &PublicKeyUse::Signature)
|| jwk.common.key_operations.as_ref().is_some_and(|operations| !operations.contains(&KeyOperations::Verify))
{
return Ok(None);
}

let decoding_key = DecodingKey::from_jwk(&jwk).map_err(AuthorizationError::InvalidKey)?;
if !matches!(decoding_key.family(), AlgorithmFamily::Rsa | AlgorithmFamily::Ec) {
return Ok(None);
}

Ok(Some(Self { key_id: jwk.common.key_id, decoding_key }))
}

pub(super) fn matches(&self, header: &Header) -> bool {
self.decoding_key.family() == header.alg.family()
&& header
.kid
.as_ref()
.is_none_or(|header_key_id| self.key_id.as_ref().is_none_or(|key_id| key_id == header_key_id))
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would carrying the declared JWK algorithm into matching cover the remaining key-selection gap?

Suggested change
pub struct VerificationKey {
pub(crate) key_id: Option<String>,
pub(crate) decoding_key: DecodingKey,
}
impl VerificationKey {
fn from_jwk(jwk: Jwk) -> Result<Option<Self>, AuthorizationError> {
if jwk.common.public_key_use.as_ref().is_some_and(|key_use| key_use != &PublicKeyUse::Signature)
|| jwk.common.key_operations.as_ref().is_some_and(|operations| !operations.contains(&KeyOperations::Verify))
{
return Ok(None);
}
let decoding_key = DecodingKey::from_jwk(&jwk).map_err(AuthorizationError::InvalidKey)?;
if !matches!(decoding_key.family(), AlgorithmFamily::Rsa | AlgorithmFamily::Ec) {
return Ok(None);
}
Ok(Some(Self { key_id: jwk.common.key_id, decoding_key }))
}
pub(super) fn matches(&self, header: &Header) -> bool {
self.decoding_key.family() == header.alg.family()
&& header
.kid
.as_ref()
.is_none_or(|header_key_id| self.key_id.as_ref().is_none_or(|key_id| key_id == header_key_id))
}
}
pub struct VerificationKey {
pub(crate) key_id: Option<String>,
pub(crate) algorithm: Option<Algorithm>,
pub(crate) decoding_key: DecodingKey,
}
impl VerificationKey {
fn from_jwk(jwk: Jwk) -> Result<Option<Self>, AuthorizationError> {
if jwk.common.public_key_use.as_ref().is_some_and(|key_use| key_use != &PublicKeyUse::Signature)
|| jwk.common.key_operations.as_ref().is_some_and(|operations| !operations.contains(&KeyOperations::Verify))
{
return Ok(None);
}
let algorithm = match jwk.common.key_algorithm {
Some(value) => match Algorithm::try_from(value) {
Ok(algorithm)
if matches!(
algorithm,
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 | Algorithm::ES256 | Algorithm::ES384
) =>
{
Some(algorithm)
},
Ok(_) | Err(_) => return Ok(None),
},
None => None,
};
let decoding_key = DecodingKey::from_jwk(&jwk).map_err(AuthorizationError::InvalidKey)?;
if !matches!(decoding_key.family(), AlgorithmFamily::Rsa | AlgorithmFamily::Ec) {
return Ok(None);
}
Ok(Some(Self { key_id: jwk.common.key_id, algorithm, decoding_key }))
}
pub(super) fn matches(&self, header: &Header) -> bool {
self.decoding_key.family() == header.alg.family()
&& self.algorithm.is_none_or(|algorithm| algorithm == header.alg)
&& header
.kid
.as_ref()
.is_none_or(|header_key_id| self.key_id.as_ref().is_none_or(|key_id| key_id == header_key_id))
}
}


pub struct DefaultPrincipalExtractor {}

pub trait PrincipalExtractor {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be the public extractor contract, followed by re-exporting it and accepting an Arc in a constructor?

Suggested change
pub trait PrincipalExtractor {
pub trait PrincipalExtractor: Send + Sync {


impl VerificationKey {
pub fn new(id: Option<String>, decoding_key: DecodingKey) -> Self {
Self { key_id: id, decoding_key }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This keeps the test-only constructor compatible with the algorithm-aware key.

Suggested change
Self { key_id: id, decoding_key }
Self { key_id: id, algorithm: None, decoding_key }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants