Skip to content

Commit

Permalink
feat: add support for azuread onbehalfof
Browse files Browse the repository at this point in the history
Co-authored-by: tronghn <trong.huu.nguyen@nav.no>
Co-authored-by: kimtore <kim.tore.jensen@nav.no>
  • Loading branch information
3 people committed Nov 1, 2024
1 parent 475c62b commit b1683cc
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 29 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ MASKINPORTEN_CLIENT_JWK='{"p":"_LNnIjBshCrFuxtjUC2KKzg_NTVv26UZh5j12_9r5mYTxb8yW
MASKINPORTEN_ISSUER=http://localhost:8080/default
MASKINPORTEN_TOKEN_ENDPOINT=http://localhost:8080/default/token
MASKINPORTEN_JWKS_URI=http://localhost:8080/default/jwks

AZURE_AD_CLIENT_ID=client-id
AZURE_AD_CLIENT_JWK='{"p":"_LNnIjBshCrFuxtjUC2KKzg_NTVv26UZh5j12_9r5mYTxb8yW047jOYFEGvIdMkTRLGOBig6fLWzgd62lnLainzV35J6K6zr4jQfTldLondlkldMR6nQrp1KfnNUuRbKvzpNKkhl12-f1l91l0tCx3s4blztvWgdzN2xBfvWV68","kty":"RSA","q":"9MIWsbIA3WjiR_Ful5FM8NCgb6JdS2D6ySHVepoNI-iAPilcltF_J2orjfLqAxeztTskPi45wtF_-eV4GIYSzvMo-gFiXLMrvEa7WaWizMi_7Bu9tEk3m_f3IDLN9lwULYoebkDbiXx6GOiuj0VkuKz8ckYFNKLCMP9QRLFff-0","d":"J6UX848X8tNz-09PFvcFDUVqak32GXzoPjnuDjBsxNUvG7LxenLmM_i8tvYl0EW9Ztn4AiCqJUoHw5cX3jz_mSqGl7ciaDedpKm_AetcZwHiEuT1EpSKRPMmOMQSqcJqXrdbbWB8gdUrnTKZIlJCfj7yqgT16ypC43TnwjA0UwxhG5pHaYjKI3pPdoHg2BzA-iubHjVn15Sz7-pnjBmeGDbEFa7ADY-1yPHCmqqvPKTNhoCNW6RpG34Id9hXslPa3X-7pAhJrDBd0_NPlktSA2rUkifYiZURhHR5ijhe0v3uw6kYP8f_foVm_C8O1ExkxXh9Dg8KDZ89dbsSOtBc0Q","e":"AQAB","use":"sig","kid":"l7C_WJgbZ_6e59vPrFETAehX7Dsp7fIyvSV4XhotsGs","qi":"cQFN5q5WhYkzgd1RS0rGqvpX1AkmZMrLv2MW04gSfu0dDwpbsSAu8EUCQW9oA4pr6V7R9CBSu9kdN2iY5SR-hZvEad5nDKPV1F3TMQYv5KpRiS_0XhfV5PcolUJVO_4p3h8d-mo2hh1Sw2fairAKOzvnwJCQ6DFkiY7H1cqwA54","dp":"YTql9AGtvyy158gh7jeXcgmySEbHQzvDFulDr-IXIg8kjHGEbp0rTIs0Z50RA95aC5RFkRjpaBKBfvaySjDm5WIi6GLzntpp6B8l7H6qG1jVO_la4Df2kzjx8LVvY8fhOrKz_hDdHodUeKdCF3RdvWMr00ruLnJhBPJHqoW7cwE","alg":"RS256","dq":"IZA4AngRbEtEtG7kJn6zWVaSmZxfRMXwvgIYvy4-3Qy2AVA0tS3XTPVfMaD8_B2U9CY_CxPVseR-sysHc_12uNBZbycfcOzU84WTjXCMSZ7BysPnGMDtkkLHra-p1L29upz1HVNhh5H9QEswHM98R2LZX2ZAsn4bORLZ1AGqweU","n":"8ZqUp5Cs90XpNn8tJBdUUxdGH4bjqKjFj8lyB3x50RpTuECuwzX1NpVqyFENDiEtMja5fdmJl6SErjnhj6kbhcmfmFibANuG-0WlV5yMysdSbocd75C1JQbiPdpHdXrijmVFMfDnoZTQ-ErNsqqngTNkn5SXBcPenli6Cf9MTSchZuh_qFj_B7Fp3CWKehTiyBcLlNOIjYsXX8WQjZkWKGpQ23AWjZulngWRektLcRWuEKTWaRBtbAr3XAfSmcqTICrebaD3IMWKHDtvzHAt_pt4wnZ06clgeO2Wbc980usnpsF7g8k9p81RcbS4JEZmuuA9NCmOmbyADXwgA9_-Aw"}'
AZURE_AD_ISSUER=http://localhost:8080/azuread
AZURE_AD_TOKEN_ENDPOINT=http://localhost:8080/azuread/token
AZURE_AD_JWKS_URI=http://localhost:8080/azuread/jwks
17 changes: 17 additions & 0 deletions hack/roundtrip-azure.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash -e
user_token_response=$(curl -s -X POST http://localhost:8080/azuread/token -d "grant_type=authorization_code&code=yolo&client_id=yolo&client_secret=bolo")
user_token=$(echo ${user_token_response} | jq -r .access_token)

response=$(curl -s -X POST http://localhost:3000/token -H "content-type: application/json" -d '{"target": "my-target", "identity_provider": "azuread", "user_token": "'${user_token}'"}')
token=$(echo ${response} | jq -r .access_token)

#validation=$(curl -s -X POST http://localhost:3000/introspection -H "content-type: application/json" -d "{\"token\": \"${token}\"}")

echo
echo "JWT:"
echo "${response}" | jq -S .

echo
echo "Validation:"
echo "${validation}" | jq -S .

18 changes: 15 additions & 3 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ pub async fn token(
) -> Result<impl IntoResponse, ApiError> {
let endpoint = state.token_endpoint(&request.identity_provider).await;
let params = state
.token_request(&request.identity_provider, request.target)
.token_request(
&request.identity_provider,
request.target,
request.user_token,
)
.await;

let client = reqwest::Client::new();
Expand Down Expand Up @@ -84,6 +88,7 @@ pub async fn introspection(
pub struct HandlerState {
pub cfg: Config,
pub maskinporten: Arc<RwLock<Maskinporten>>,
pub azure_ad: Arc<RwLock<AzureAD>>,
// TODO: other providers
}

Expand All @@ -92,9 +97,16 @@ impl HandlerState {
&self,
identity_provider: &IdentityProvider,
target: String,
user_token: Option<String>,
) -> Box<dyn erased_serde::Serialize + Send> {
match identity_provider {
IdentityProvider::EntraID => todo!(),
IdentityProvider::AzureAD => {
if let Some(x) = user_token {
Box::new(self.azure_ad.read().await.on_behalf_of_request(target, x))
} else {
Box::new(self.azure_ad.read().await.token_request(target))
}
}
IdentityProvider::TokenX => todo!(),
IdentityProvider::Maskinporten => {
Box::new(self.maskinporten.read().await.token_request(target))
Expand All @@ -104,7 +116,7 @@ impl HandlerState {

async fn token_endpoint(&self, identity_provider: &IdentityProvider) -> String {
match identity_provider {
IdentityProvider::EntraID => todo!(),
IdentityProvider::AzureAD => self.azure_ad.read().await.token_endpoint(),
IdentityProvider::TokenX => todo!(),
IdentityProvider::Maskinporten => self.maskinporten.read().await.token_endpoint(),
}
Expand Down
147 changes: 123 additions & 24 deletions src/identity_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::config::Config;
use crate::jwks;
use jsonwebkey as jwk;
use jsonwebtoken as jwt;
use jsonwebtoken::{EncodingKey, Header};
use serde::Serialize;
use serde_json::Value;
use std::collections::HashMap;
Expand All @@ -23,22 +24,95 @@ pub struct Maskinporten {
upstream_jwks: jwks::Jwks,
}

#[derive(Clone, Debug)]
pub struct EntraID(pub Config);
#[derive(Clone)]
pub struct AzureAD {
pub cfg: Config,
private_jwk: jwt::EncodingKey,
client_assertion_header: jwt::Header,
upstream_jwks: jwks::Jwks,
}

#[derive(Clone, Debug)]
pub struct TokenX(pub Config);

#[derive(Serialize)]
pub struct EntraIDTokenRequest {}
pub struct AzureADClientCredentialsTokenRequest {
grant_type: String, // client_credentials
client_id: String,
client_assertion: String,
client_assertion_type: String, // urn:ietf:params:oauth:client-assertion-type:jwt-bearer
scope: String,
}

#[derive(Serialize)]
pub struct AzureADOnBehalfOfTokenRequest {
grant_type: String, // urn:ietf:params:oauth:grant-type:jwt-bearer
client_id: String,
client_assertion: String,
client_assertion_type: String, // urn:ietf:params:oauth:client-assertion-type:jwt-bearer
scope: String,
requested_token_use: String, // on_behalf_of
assertion: String,
}

impl AzureAD {
pub fn on_behalf_of_request(
&self,
target: String,
user_token: String,
) -> AzureADOnBehalfOfTokenRequest {
let client_assertion = AssertionClaims::new(
self.cfg.azure_ad_issuer.clone(),
self.cfg.azure_ad_client_id.clone(),
None,
Some(self.cfg.azure_ad_client_id.clone()),
)
.serialize(&self.client_assertion_header, &self.private_jwk)
.unwrap();

AzureADOnBehalfOfTokenRequest {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer".to_string(),
client_id: self.cfg.azure_ad_client_id.clone(),
client_assertion,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
.to_string(),
scope: target,
requested_token_use: "on_behalf_of".to_string(),
assertion: user_token,
}
}

pub fn new(cfg: Config, upstream_jwks: jwks::Jwks) -> Self {
let client_private_jwk: jwk::JsonWebKey = cfg.azure_ad_client_jwk.parse().unwrap();
let alg: jwt::Algorithm = client_private_jwk.algorithm.unwrap().into();
let kid: String = client_private_jwk.key_id.clone().unwrap();

let mut header = jwt::Header::new(alg);
header.kid = Some(kid);

Self {
cfg,
upstream_jwks,
private_jwk: client_private_jwk.key.to_encoding_key(),
client_assertion_header: header,
}
}
}

impl Provider<EntraIDTokenRequest> for EntraID {
fn token_request(&self, _target: String) -> EntraIDTokenRequest {
EntraIDTokenRequest {}
impl Provider<AzureADClientCredentialsTokenRequest> for AzureAD {
fn token_request(&self, _target: String) -> AzureADClientCredentialsTokenRequest {
AzureADClientCredentialsTokenRequest {
grant_type: "client_credentials".to_string(),
client_id: self.cfg.maskinporten_client_id.clone(),
client_assertion: "".to_string(),
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
.to_string(),
scope: "".to_string(),
}
}

fn token_endpoint(&self) -> String {
todo!()
self.cfg.azure_ad_token_endpoint.to_string()
}

async fn introspect(&mut self, _token: String) -> HashMap<String, Value> {
Expand Down Expand Up @@ -71,22 +145,14 @@ pub struct MaskinportenTokenRequest {

impl Provider<MaskinportenTokenRequest> for Maskinporten {
fn token_request(&self, target: String) -> MaskinportenTokenRequest {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let jti = uuid::Uuid::new_v4();

let claims = AssertionClaims {
exp: (now + 30) as usize,
iat: now as usize,
jti: jti.to_string(),
scope: target.to_string(),
iss: self.cfg.maskinporten_client_id.to_string(),
aud: self.cfg.maskinporten_issuer.to_string(),
};

let token = jwt::encode(&self.client_assertion_header, &claims, &self.private_jwk).unwrap();
let token = AssertionClaims::new(
self.cfg.maskinporten_issuer.clone(),
self.cfg.maskinporten_client_id.clone(),
Some(target),
None,
)
.serialize(&self.client_assertion_header, &self.private_jwk)
.unwrap();

MaskinportenTokenRequest {
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer".to_string(),
Expand Down Expand Up @@ -137,8 +203,41 @@ impl Maskinporten {
struct AssertionClaims {
exp: usize,
iat: usize,
nbf: usize,
jti: String,
scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
scope: Option<String>,
iss: String,
aud: String,
#[serde(skip_serializing_if = "Option::is_none")]
sub: Option<String>,
}

impl AssertionClaims {
fn new(issuer: String, client_id: String, scope: Option<String>, sub: Option<String>) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let jti = uuid::Uuid::new_v4();

AssertionClaims {
exp: (now + 30) as usize,
iat: now as usize,
nbf: now as usize,
jti: jti.to_string(),
scope,
sub,
iss: client_id, // issuer of the token is the client itself
aud: issuer, // audience of the token is the issuer
}
}

fn serialize(
&self,
client_assertion_header: &Header,
key: &EncodingKey,
) -> Result<String, jsonwebtoken::errors::Error> {
jwt::encode(client_assertion_header, &self, key)
}
}
18 changes: 18 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ pub mod config {
pub maskinporten_issuer: String,
#[arg(env)]
pub maskinporten_token_endpoint: String,
#[arg(env)]
pub azure_ad_client_id: String,
#[arg(env)]
pub azure_ad_client_jwk: String,
#[arg(env)]
pub azure_ad_jwks_uri: String,
#[arg(env)]
pub azure_ad_issuer: String,
#[arg(env)]
pub azure_ad_token_endpoint: String,
}
}

Expand Down Expand Up @@ -69,9 +79,17 @@ async fn main() {
.unwrap(),
);

let azure_ad = identity_provider::AzureAD::new(
cfg.clone(),
jwks::Jwks::new(&cfg.azure_ad_issuer, &cfg.azure_ad_jwks_uri)
.await
.unwrap(),
);

let state = handlers::HandlerState {
cfg: cfg.clone(),
maskinporten: Arc::new(RwLock::new(maskinporten)),
azure_ad: Arc::new(RwLock::new(azure_ad)),
};

let app = Router::new()
Expand Down
4 changes: 2 additions & 2 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ pub struct IntrospectRequest {

#[derive(Deserialize, Serialize, Clone, Debug)]
pub enum IdentityProvider {
#[serde(rename = "entra")]
EntraID,
#[serde(rename = "azuread")]
AzureAD,
#[serde(rename = "tokenx")]
TokenX,
#[serde(rename = "maskinporten")]
Expand Down

0 comments on commit b1683cc

Please sign in to comment.