Skip to content

Commit

Permalink
feat: add support for resource indicator
Browse files Browse the repository at this point in the history
Currently only supported for the token-endpoint.
Should be fairly easy to add to token exchange endpoint in the future,
if needed.

Also reword some docstring comments for clarification.

Co-authored-by: Kim Tore Jensen <kimtjen@gmail.com>
  • Loading branch information
tronghn and kimtore committed Nov 25, 2024
1 parent 156f3bc commit 83431f4
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 17 deletions.
14 changes: 11 additions & 3 deletions doc/openapi-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"Generate a token for Maskinporten": {
"value": {
"identity_provider": "maskinporten",
"resource": "http://resource.example/api",
"target": "altinn:serviceowner/rolesandrights"
}
}
Expand Down Expand Up @@ -286,7 +287,7 @@
},
"IntrospectResponse": {
"type": "object",
"description": "RFC 7662 introspection response from section 2.2.\n\nIdentity provider's claims differ from one another.\nPlease refer to the Nais documentation for details:\n\n- [Azure AD](https://doc.nais.io/auth/entra-id/reference/#claims)\n- [IDPorten](https://doc.nais.io/auth/idporten/reference/#claims)\n- [Maskinporten](https://doc.nais.io/auth/maskinporten/reference/#claims)\n- [TokenX](https://doc.nais.io/auth/tokenx/reference/#claims)",
"description": "Loosely based on RFC 7662 introspection response from section 2.2.\n\nClaims from the original token are copied verbatim to the introspection response as additional properties.\nThe claims present depend on the identity provider.\nPlease refer to the Nais documentation for details:\n\n- [Azure AD](https://doc.nais.io/auth/entra-id/reference/#claims)\n- [IDPorten](https://doc.nais.io/auth/idporten/reference/#claims)\n- [Maskinporten](https://doc.nais.io/auth/maskinporten/reference/#claims)\n- [TokenX](https://doc.nais.io/auth/tokenx/reference/#claims)",
"required": [
"active"
],
Expand Down Expand Up @@ -333,7 +334,7 @@
},
"target": {
"type": "string",
"description": "The issued token will only be accepted by the targeted application, specified in this field."
"description": "Scope or identifier for the target application."
},
"user_token": {
"type": "string",
Expand All @@ -352,9 +353,16 @@
"identity_provider": {
"$ref": "#/components/schemas/IdentityProvider"
},
"resource": {
"type": [
"string",
"null"
],
"description": "Resource indicator for audience-restricted tokens (RFC 8707)."
},
"target": {
"type": "string",
"description": "The issued token will only be accepted by the targeted application, specified in this field."
"description": "Scope or identifier for the target application."
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion hack/roundtrip-maskinporten.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/bash -e
response=$(curl -s -X POST http://localhost:3000/api/v1/token -H "content-type: application/json" -d '{"target": "my-target", "identity_provider": "maskinporten"}')
response=$(curl -s -X POST http://localhost:3000/api/v1/token -H "content-type: application/json" -d '{"target": "my-target", "identity_provider": "maskinporten", "resource": "some-resource"}')
token=$(echo ${response} | jq -r .access_token)
validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\", \"identity_provider\": \"maskinporten\"}")

Expand Down
7 changes: 6 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ mod tests {
TokenRequest {
target: "invalid".to_string(),
identity_provider: IdentityProvider::AzureAD,
resource: None,
},
Json,
)
Expand Down Expand Up @@ -586,7 +587,11 @@ mod tests {
async fn machine_to_machine_token(expected_issuer: String, target: String, address: String, identity_provider: IdentityProvider, request_format: RequestFormat) {
let response = post_request(
format!("http://{}/api/v1/token", address.clone().to_string()),
TokenRequest { target, identity_provider },
TokenRequest {
target,
identity_provider,
resource: None
},
request_format.clone(),
)
.await
Expand Down
11 changes: 7 additions & 4 deletions src/claims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::Serialize;
const EXPIRY_LEEWAY_SECONDS: usize = 30;

pub trait Assertion: Send + Sync + Serialize {
fn new(token_endpoint: String, client_id: String, target: String) -> Self;
fn new(token_endpoint: String, client_id: String, target: String, resource: Option<String>) -> Self;
}

#[derive(Serialize)]
Expand All @@ -27,10 +27,12 @@ pub struct JWTBearerAssertion {
scope: String,
iss: String,
aud: String,
#[serde(skip_serializing_if = "Option::is_none")]
resource: Option<String>,
}

impl Assertion for JWTBearerAssertion {
fn new(token_endpoint: String, client_id: String, target: String) -> Self {
fn new(token_endpoint: String, client_id: String, target: String, resource: Option<String>) -> Self {
let now = epoch_now_secs();
let jti = uuid::Uuid::new_v4();

Expand All @@ -42,12 +44,13 @@ impl Assertion for JWTBearerAssertion {
iss: client_id, // issuer of the token is the client itself
aud: token_endpoint, // audience of the token is the issuer
scope: target,
resource, // resource indicator for audience-restricted tokens
}
}
}

impl Assertion for ClientAssertion {
fn new(token_endpoint: String, client_id: String, _target: String) -> Self {
fn new(token_endpoint: String, client_id: String, _target: String, _resource: Option<String>) -> Self {
let now = epoch_now_secs();
let jti = uuid::Uuid::new_v4();

Expand All @@ -64,7 +67,7 @@ impl Assertion for ClientAssertion {
}

impl Assertion for () {
fn new(_token_endpoint: String, _client_id: String, _target: String) -> Self {}
fn new(_token_endpoint: String, _client_id: String, _target: String, _resource: Option<String>) -> Self {}
}

pub fn epoch_now_secs() -> u64 {
Expand Down
2 changes: 2 additions & 0 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ use tracing::instrument;
("Generate a token for Maskinporten" = (value = json!(TokenRequest{
identity_provider: IdentityProvider::Maskinporten,
target: "altinn:serviceowner/rolesandrights".to_string(),
resource: Some("http://resource.example/api".to_string()),
}))),
("Generate a token for Azure AD" = (value = json!(TokenRequest{
identity_provider: IdentityProvider::AzureAD,
target: "api://cluster.namespace.application/.default".to_string(),
resource: None,
}))),
),
),
Expand Down
20 changes: 12 additions & 8 deletions src/identity_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ pub enum TokenType {
Bearer,
}

/// RFC 7662 introspection response from section 2.2.
/// Based on RFC 7662 introspection response from section 2.2.
///
/// Identity provider's claims differ from one another.
/// Claims from the original token are copied verbatim to the introspection response as additional properties.
/// The claims present depend on the identity provider.
/// Please refer to the Nais documentation for details:
///
/// - [Azure AD](https://doc.nais.io/auth/entra-id/reference/#claims)
Expand Down Expand Up @@ -147,15 +148,18 @@ impl Display for IdentityProvider {
/// Use this data type to request a machine token.
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
pub struct TokenRequest {
/// The issued token will only be accepted by the targeted application, specified in this field.
/// Scope or identifier for the target application.
pub target: String,
pub identity_provider: IdentityProvider,
/// Resource indicator for audience-restricted tokens (RFC 8707).
#[serde(skip_serializing_if = "Option::is_none")]
pub resource: Option<String>,
}

/// Use this data type to exchange a user token for a machine token.
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
pub struct TokenExchangeRequest {
/// The issued token will only be accepted by the targeted application, specified in this field.
/// Scope or identifier for the target application.
pub target: String,
pub identity_provider: IdentityProvider,

Expand Down Expand Up @@ -265,8 +269,8 @@ where
}

#[instrument(skip_all, name = "Create assertion for token signing request")]
fn create_assertion(&self, target: String) -> Option<String> {
let assertion = A::new(self.token_endpoint.as_ref()?.clone(), self.client_id.clone(), target);
fn create_assertion(&self, target: String, resource: Option<String>) -> Option<String> {
let assertion = A::new(self.token_endpoint.as_ref()?.clone(), self.client_id.clone(), target, resource);
serialize(assertion, self.client_assertion_header.as_ref()?, self.private_jwk.as_ref()?).ok()
}
}
Expand All @@ -281,7 +285,7 @@ where
async fn get_token(&self, request: TokenRequest) -> Result<TokenResponse, ApiError> {
let token_request = TokenRequestBuilderParams {
target: request.target.clone(),
assertion: self.create_assertion(request.target).ok_or(ApiError::TokenRequestUnsupported(self.identity_provider_kind))?,
assertion: self.create_assertion(request.target, request.resource).ok_or(ApiError::TokenRequestUnsupported(self.identity_provider_kind))?,
client_id: Some(self.client_id.clone()),
user_token: None,
};
Expand All @@ -291,7 +295,7 @@ where
async fn exchange_token(&self, request: TokenExchangeRequest) -> Result<TokenResponse, ApiError> {
let token_request = TokenRequestBuilderParams {
target: request.target.clone(),
assertion: self.create_assertion(request.target).ok_or(ApiError::TokenExchangeUnsupported(self.identity_provider_kind))?,
assertion: self.create_assertion(request.target, None).ok_or(ApiError::TokenExchangeUnsupported(self.identity_provider_kind))?,
client_id: Some(self.client_id.clone()),
user_token: Some(request.user_token),
};
Expand Down

0 comments on commit 83431f4

Please sign in to comment.