Skip to content

Commit b1683cc

Browse files
tommytroentronghnkimtore
committed
feat: add support for azuread onbehalfof
Co-authored-by: tronghn <[email protected]> Co-authored-by: kimtore <[email protected]>
1 parent 475c62b commit b1683cc

File tree

6 files changed

+181
-29
lines changed

6 files changed

+181
-29
lines changed

.env.example

+6
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ MASKINPORTEN_CLIENT_JWK='{"p":"_LNnIjBshCrFuxtjUC2KKzg_NTVv26UZh5j12_9r5mYTxb8yW
55
MASKINPORTEN_ISSUER=http://localhost:8080/default
66
MASKINPORTEN_TOKEN_ENDPOINT=http://localhost:8080/default/token
77
MASKINPORTEN_JWKS_URI=http://localhost:8080/default/jwks
8+
9+
AZURE_AD_CLIENT_ID=client-id
10+
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"}'
11+
AZURE_AD_ISSUER=http://localhost:8080/azuread
12+
AZURE_AD_TOKEN_ENDPOINT=http://localhost:8080/azuread/token
13+
AZURE_AD_JWKS_URI=http://localhost:8080/azuread/jwks

hack/roundtrip-azure.sh

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash -e
2+
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")
3+
user_token=$(echo ${user_token_response} | jq -r .access_token)
4+
5+
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}'"}')
6+
token=$(echo ${response} | jq -r .access_token)
7+
8+
#validation=$(curl -s -X POST http://localhost:3000/introspection -H "content-type: application/json" -d "{\"token\": \"${token}\"}")
9+
10+
echo
11+
echo "JWT:"
12+
echo "${response}" | jq -S .
13+
14+
echo
15+
echo "Validation:"
16+
echo "${validation}" | jq -S .
17+

src/handlers.rs

+15-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ pub async fn token(
2525
) -> Result<impl IntoResponse, ApiError> {
2626
let endpoint = state.token_endpoint(&request.identity_provider).await;
2727
let params = state
28-
.token_request(&request.identity_provider, request.target)
28+
.token_request(
29+
&request.identity_provider,
30+
request.target,
31+
request.user_token,
32+
)
2933
.await;
3034

3135
let client = reqwest::Client::new();
@@ -84,6 +88,7 @@ pub async fn introspection(
8488
pub struct HandlerState {
8589
pub cfg: Config,
8690
pub maskinporten: Arc<RwLock<Maskinporten>>,
91+
pub azure_ad: Arc<RwLock<AzureAD>>,
8792
// TODO: other providers
8893
}
8994

@@ -92,9 +97,16 @@ impl HandlerState {
9297
&self,
9398
identity_provider: &IdentityProvider,
9499
target: String,
100+
user_token: Option<String>,
95101
) -> Box<dyn erased_serde::Serialize + Send> {
96102
match identity_provider {
97-
IdentityProvider::EntraID => todo!(),
103+
IdentityProvider::AzureAD => {
104+
if let Some(x) = user_token {
105+
Box::new(self.azure_ad.read().await.on_behalf_of_request(target, x))
106+
} else {
107+
Box::new(self.azure_ad.read().await.token_request(target))
108+
}
109+
}
98110
IdentityProvider::TokenX => todo!(),
99111
IdentityProvider::Maskinporten => {
100112
Box::new(self.maskinporten.read().await.token_request(target))
@@ -104,7 +116,7 @@ impl HandlerState {
104116

105117
async fn token_endpoint(&self, identity_provider: &IdentityProvider) -> String {
106118
match identity_provider {
107-
IdentityProvider::EntraID => todo!(),
119+
IdentityProvider::AzureAD => self.azure_ad.read().await.token_endpoint(),
108120
IdentityProvider::TokenX => todo!(),
109121
IdentityProvider::Maskinporten => self.maskinporten.read().await.token_endpoint(),
110122
}

src/identity_provider.rs

+123-24
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::config::Config;
22
use crate::jwks;
33
use jsonwebkey as jwk;
44
use jsonwebtoken as jwt;
5+
use jsonwebtoken::{EncodingKey, Header};
56
use serde::Serialize;
67
use serde_json::Value;
78
use std::collections::HashMap;
@@ -23,22 +24,95 @@ pub struct Maskinporten {
2324
upstream_jwks: jwks::Jwks,
2425
}
2526

26-
#[derive(Clone, Debug)]
27-
pub struct EntraID(pub Config);
27+
#[derive(Clone)]
28+
pub struct AzureAD {
29+
pub cfg: Config,
30+
private_jwk: jwt::EncodingKey,
31+
client_assertion_header: jwt::Header,
32+
upstream_jwks: jwks::Jwks,
33+
}
2834

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

3238
#[derive(Serialize)]
33-
pub struct EntraIDTokenRequest {}
39+
pub struct AzureADClientCredentialsTokenRequest {
40+
grant_type: String, // client_credentials
41+
client_id: String,
42+
client_assertion: String,
43+
client_assertion_type: String, // urn:ietf:params:oauth:client-assertion-type:jwt-bearer
44+
scope: String,
45+
}
46+
47+
#[derive(Serialize)]
48+
pub struct AzureADOnBehalfOfTokenRequest {
49+
grant_type: String, // urn:ietf:params:oauth:grant-type:jwt-bearer
50+
client_id: String,
51+
client_assertion: String,
52+
client_assertion_type: String, // urn:ietf:params:oauth:client-assertion-type:jwt-bearer
53+
scope: String,
54+
requested_token_use: String, // on_behalf_of
55+
assertion: String,
56+
}
57+
58+
impl AzureAD {
59+
pub fn on_behalf_of_request(
60+
&self,
61+
target: String,
62+
user_token: String,
63+
) -> AzureADOnBehalfOfTokenRequest {
64+
let client_assertion = AssertionClaims::new(
65+
self.cfg.azure_ad_issuer.clone(),
66+
self.cfg.azure_ad_client_id.clone(),
67+
None,
68+
Some(self.cfg.azure_ad_client_id.clone()),
69+
)
70+
.serialize(&self.client_assertion_header, &self.private_jwk)
71+
.unwrap();
72+
73+
AzureADOnBehalfOfTokenRequest {
74+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer".to_string(),
75+
client_id: self.cfg.azure_ad_client_id.clone(),
76+
client_assertion,
77+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
78+
.to_string(),
79+
scope: target,
80+
requested_token_use: "on_behalf_of".to_string(),
81+
assertion: user_token,
82+
}
83+
}
84+
85+
pub fn new(cfg: Config, upstream_jwks: jwks::Jwks) -> Self {
86+
let client_private_jwk: jwk::JsonWebKey = cfg.azure_ad_client_jwk.parse().unwrap();
87+
let alg: jwt::Algorithm = client_private_jwk.algorithm.unwrap().into();
88+
let kid: String = client_private_jwk.key_id.clone().unwrap();
89+
90+
let mut header = jwt::Header::new(alg);
91+
header.kid = Some(kid);
92+
93+
Self {
94+
cfg,
95+
upstream_jwks,
96+
private_jwk: client_private_jwk.key.to_encoding_key(),
97+
client_assertion_header: header,
98+
}
99+
}
100+
}
34101

35-
impl Provider<EntraIDTokenRequest> for EntraID {
36-
fn token_request(&self, _target: String) -> EntraIDTokenRequest {
37-
EntraIDTokenRequest {}
102+
impl Provider<AzureADClientCredentialsTokenRequest> for AzureAD {
103+
fn token_request(&self, _target: String) -> AzureADClientCredentialsTokenRequest {
104+
AzureADClientCredentialsTokenRequest {
105+
grant_type: "client_credentials".to_string(),
106+
client_id: self.cfg.maskinporten_client_id.clone(),
107+
client_assertion: "".to_string(),
108+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
109+
.to_string(),
110+
scope: "".to_string(),
111+
}
38112
}
39113

40114
fn token_endpoint(&self) -> String {
41-
todo!()
115+
self.cfg.azure_ad_token_endpoint.to_string()
42116
}
43117

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

72146
impl Provider<MaskinportenTokenRequest> for Maskinporten {
73147
fn token_request(&self, target: String) -> MaskinportenTokenRequest {
74-
let now = std::time::SystemTime::now()
75-
.duration_since(std::time::UNIX_EPOCH)
76-
.unwrap()
77-
.as_secs();
78-
let jti = uuid::Uuid::new_v4();
79-
80-
let claims = AssertionClaims {
81-
exp: (now + 30) as usize,
82-
iat: now as usize,
83-
jti: jti.to_string(),
84-
scope: target.to_string(),
85-
iss: self.cfg.maskinporten_client_id.to_string(),
86-
aud: self.cfg.maskinporten_issuer.to_string(),
87-
};
88-
89-
let token = jwt::encode(&self.client_assertion_header, &claims, &self.private_jwk).unwrap();
148+
let token = AssertionClaims::new(
149+
self.cfg.maskinporten_issuer.clone(),
150+
self.cfg.maskinporten_client_id.clone(),
151+
Some(target),
152+
None,
153+
)
154+
.serialize(&self.client_assertion_header, &self.private_jwk)
155+
.unwrap();
90156

91157
MaskinportenTokenRequest {
92158
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer".to_string(),
@@ -137,8 +203,41 @@ impl Maskinporten {
137203
struct AssertionClaims {
138204
exp: usize,
139205
iat: usize,
206+
nbf: usize,
140207
jti: String,
141-
scope: String,
208+
#[serde(skip_serializing_if = "Option::is_none")]
209+
scope: Option<String>,
142210
iss: String,
143211
aud: String,
212+
#[serde(skip_serializing_if = "Option::is_none")]
213+
sub: Option<String>,
214+
}
215+
216+
impl AssertionClaims {
217+
fn new(issuer: String, client_id: String, scope: Option<String>, sub: Option<String>) -> Self {
218+
let now = std::time::SystemTime::now()
219+
.duration_since(std::time::UNIX_EPOCH)
220+
.unwrap()
221+
.as_secs();
222+
let jti = uuid::Uuid::new_v4();
223+
224+
AssertionClaims {
225+
exp: (now + 30) as usize,
226+
iat: now as usize,
227+
nbf: now as usize,
228+
jti: jti.to_string(),
229+
scope,
230+
sub,
231+
iss: client_id, // issuer of the token is the client itself
232+
aud: issuer, // audience of the token is the issuer
233+
}
234+
}
235+
236+
fn serialize(
237+
&self,
238+
client_assertion_header: &Header,
239+
key: &EncodingKey,
240+
) -> Result<String, jsonwebtoken::errors::Error> {
241+
jwt::encode(client_assertion_header, &self, key)
242+
}
144243
}

src/main.rs

+18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ pub mod config {
3030
pub maskinporten_issuer: String,
3131
#[arg(env)]
3232
pub maskinporten_token_endpoint: String,
33+
#[arg(env)]
34+
pub azure_ad_client_id: String,
35+
#[arg(env)]
36+
pub azure_ad_client_jwk: String,
37+
#[arg(env)]
38+
pub azure_ad_jwks_uri: String,
39+
#[arg(env)]
40+
pub azure_ad_issuer: String,
41+
#[arg(env)]
42+
pub azure_ad_token_endpoint: String,
3343
}
3444
}
3545

@@ -69,9 +79,17 @@ async fn main() {
6979
.unwrap(),
7080
);
7181

82+
let azure_ad = identity_provider::AzureAD::new(
83+
cfg.clone(),
84+
jwks::Jwks::new(&cfg.azure_ad_issuer, &cfg.azure_ad_jwks_uri)
85+
.await
86+
.unwrap(),
87+
);
88+
7289
let state = handlers::HandlerState {
7390
cfg: cfg.clone(),
7491
maskinporten: Arc::new(RwLock::new(maskinporten)),
92+
azure_ad: Arc::new(RwLock::new(azure_ad)),
7593
};
7694

7795
let app = Router::new()

src/types.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ pub struct IntrospectRequest {
5656

5757
#[derive(Deserialize, Serialize, Clone, Debug)]
5858
pub enum IdentityProvider {
59-
#[serde(rename = "entra")]
60-
EntraID,
59+
#[serde(rename = "azuread")]
60+
AzureAD,
6161
#[serde(rename = "tokenx")]
6262
TokenX,
6363
#[serde(rename = "maskinporten")]

0 commit comments

Comments
 (0)