From f1e7129edc598f78c5915978d2dcc0c998e77e40 Mon Sep 17 00:00:00 2001 From: Trong Huu Nguyen Date: Tue, 12 Nov 2024 14:01:25 +0100 Subject: [PATCH] feat: add idporten provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tommy Trøen Co-authored-by: Kim Tore Jensen --- .env.example | 6 ++++ hack/roundtrip-idporten.sh | 13 ++++++++ src/app.rs | 66 ++++++++++++++++++++++++++++++++++++-- src/claims.rs | 6 ++++ src/config.rs | 48 +++++++++++++++++++-------- src/grants.rs | 6 ++++ src/handlers.rs | 10 ++++++ src/identity_provider.rs | 57 +++++++++++++++++++++----------- 8 files changed, 177 insertions(+), 35 deletions(-) create mode 100755 hack/roundtrip-idporten.sh diff --git a/.env.example b/.env.example index ae59532..b1d5862 100644 --- a/.env.example +++ b/.env.example @@ -20,3 +20,9 @@ TOKEN_X_CLIENT_JWK='{"p":"_LNnIjBshCrFuxtjUC2KKzg_NTVv26UZh5j12_9r5mYTxb8yW047jO TOKEN_X_ISSUER=http://localhost:8080/tokenx TOKEN_X_TOKEN_ENDPOINT=http://localhost:8080/tokenx/token TOKEN_X_JWKS_URI=http://localhost:8080/tokenx/jwks + +IDPORTEN_ENABLED=true +# expected audience for tokens returned by mock-oauth2-server +IDPORTEN_AUDIENCE=default +IDPORTEN_ISSUER=http://localhost:8080/idporten +IDPORTEN_JWKS_URI=http://localhost:8080/idporten/jwks diff --git a/hack/roundtrip-idporten.sh b/hack/roundtrip-idporten.sh new file mode 100755 index 0000000..43d47b3 --- /dev/null +++ b/hack/roundtrip-idporten.sh @@ -0,0 +1,13 @@ +#!/bin/bash -e +user_token_response=$(curl -s -X POST http://localhost:8080/idporten/token -d "grant_type=authorization_code&code=yolo&client_id=yolo&client_secret=bolo") +user_token=$(echo ${user_token_response} | jq -r .access_token) + +validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${user_token}\"}") + +echo +echo "User token:" +echo "${user_token}" + +echo +echo "Validation:" +echo "${validation}" | jq -S . diff --git a/src/app.rs b/src/app.rs index ffa8a88..7647e44 100644 --- a/src/app.rs +++ b/src/app.rs @@ -146,9 +146,16 @@ mod tests { address.to_string(), identity_provider_address.to_string(), IdentityProvider::TokenX, - format, + format.clone(), ) .await; + + introspect_idporten_token( + testapp.cfg.idporten.clone().unwrap().issuer.clone(), + address.to_string(), + identity_provider_address.to_string(), + format, + ).await; } test_token_invalid_identity_provider(&address).await; @@ -406,7 +413,7 @@ mod tests { assert_eq!(response.status(), 400); assert_eq!( response.text().await.unwrap(), - r#"{"error":"invalid_request","error_description":"Failed to deserialize the JSON body into the target type: identity_provider: unknown variant `invalid`, expected one of `azuread`, `tokenx`, `maskinporten` at line 1 column 30"}"# + r#"{"error":"invalid_request","error_description":"Failed to deserialize the JSON body into the target type: identity_provider: unknown variant `invalid`, expected one of `azuread`, `tokenx`, `maskinporten`, `idporten` at line 1 column 30"}"# ); let response = post_request( @@ -420,7 +427,7 @@ mod tests { assert_eq!(response.status(), 400); assert_eq!( response.text().await.unwrap(), - r#"{"error":"invalid_request","error_description":"Failed to deserialize form body: unknown variant `invalid`, expected one of `azuread`, `tokenx`, `maskinporten`"}"# + r#"{"error":"invalid_request","error_description":"Failed to deserialize form body: unknown variant `invalid`, expected one of `azuread`, `tokenx`, `maskinporten`, `idporten`"}"# ); } @@ -485,6 +492,10 @@ mod tests { assert_eq!(body["iss"], Value::String(expected_issuer.to_string())); } + /// this tests the full token exchange roundtrip: + /// 1. fetch user token from mock-oauth2-server + /// 2. exchange user token for on-behalf-of token at /token/exchange + /// 3. introspect the resulting token at /introspect async fn token_exchange_token( expected_issuer: String, target: String, @@ -561,6 +572,55 @@ mod tests { assert!(!body["sub"].to_string().is_empty()); } + async fn introspect_idporten_token( + expected_issuer: String, + address: String, + identity_provider_address: String, + request_format: RequestFormat, + ) { + #[derive(Serialize)] + struct AuthorizeRequest { + grant_type: String, + code: String, + client_id: String, + client_secret: String, + } + + // This request goes directly to the mock oauth2 server, which only accepts form encoding + let user_token_response = post_request( + format!("http://{}/idporten/token", identity_provider_address), + AuthorizeRequest { + grant_type: "authorization_code".to_string(), + code: "mycode".to_string(), + client_id: "myclientid".to_string(), + client_secret: "myclientsecret".to_string(), + }, + RequestFormat::Form, + ) + .await + .unwrap(); + + assert_eq!(user_token_response.status(), 200); + let user_token: TokenResponse = user_token_response.json().await.unwrap(); + + let response = post_request( + format!("http://{}/api/v1/introspect", address.clone().to_string()), + IntrospectRequest { + token: user_token.access_token.clone(), + }, + request_format, + ) + .await + .unwrap(); + + assert_eq!(response.status(), 200); + let body: HashMap = response.json().await.unwrap(); + assert_eq!(body["active"], Value::Bool(true)); + assert_eq!(body.contains_key("error"), false); + assert_eq!(body["iss"], Value::String(expected_issuer.to_string())); + assert!(!body["sub"].to_string().is_empty()); + } + #[derive(Clone)] enum RequestFormat { Json, diff --git a/src/claims.rs b/src/claims.rs index edda269..4dbe3db 100644 --- a/src/claims.rs +++ b/src/claims.rs @@ -63,6 +63,12 @@ impl Assertion for ClientAssertion { } } +impl Assertion for () { + fn new(_token_endpoint: String, _client_id: String, _target: String) -> Self { + () + } +} + pub fn epoch_now_secs() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) diff --git a/src/config.rs b/src/config.rs index 838d890..e4c3b44 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,15 +10,16 @@ pub struct Config { pub maskinporten: Option, pub azure_ad: Option, pub token_x: Option, + pub idporten: Option, } #[derive(Serialize, Clone, Debug)] pub struct Provider { pub client_id: String, - pub client_jwk: String, + pub client_jwk: Option, pub jwks_uri: String, pub issuer: String, - pub token_endpoint: String, + pub token_endpoint: Option, } #[derive(Error, Debug)] @@ -46,10 +47,10 @@ impl Provider { } Ok(Some(Self { client_id: Self::must_read_env(&format!("{prefix}_CLIENT_ID"))?, - client_jwk: Self::must_read_env(&format!("{prefix}_CLIENT_JWK"))?, + client_jwk: Some(Self::must_read_env(&format!("{prefix}_CLIENT_JWK"))?), jwks_uri: Self::must_read_env(&format!("{prefix}_JWKS_URI"))?, issuer: Self::must_read_env(&format!("{prefix}_ISSUER"))?, - token_endpoint: Self::must_read_env(&format!("{prefix}_TOKEN_ENDPOINT"))?, + token_endpoint: Some(Self::must_read_env(&format!("{prefix}_TOKEN_ENDPOINT"))?), })) } @@ -59,10 +60,23 @@ impl Provider { } Ok(Some(Self { client_id: Self::must_read_env("AZURE_APP_CLIENT_ID")?, - client_jwk: Self::must_read_env("AZURE_APP_CLIENT_JWK")?, + client_jwk: Some(Self::must_read_env("AZURE_APP_CLIENT_JWK")?), jwks_uri: Self::must_read_env("AZURE_OPENID_CONFIG_JWKS_URI")?, issuer: Self::must_read_env("AZURE_OPENID_CONFIG_ISSUER")?, - token_endpoint: Self::must_read_env("AZURE_OPENID_CONFIG_TOKEN_ENDPOINT")?, + token_endpoint: Some(Self::must_read_env("AZURE_OPENID_CONFIG_TOKEN_ENDPOINT")?), + })) + } + + fn new_from_idporten_env() -> Result, Error> { + if !Self::is_provider_enabled("IDPORTEN")? { + return Ok(None); + } + Ok(Some(Self { + client_id: Self::must_read_env("IDPORTEN_AUDIENCE")?, + client_jwk: None, + jwks_uri: Self::must_read_env("IDPORTEN_JWKS_URI")?, + issuer: Self::must_read_env("IDPORTEN_ISSUER")?, + token_endpoint: None, })) } @@ -95,6 +109,7 @@ impl Config { azure_ad: Provider::new_from_azure_env()?, maskinporten: Provider::new_from_env_with_prefix("MASKINPORTEN")?, token_x: Provider::new_from_env_with_prefix("TOKEN_X")?, + idporten: Provider::new_from_idporten_env()?, }) } } @@ -107,24 +122,31 @@ impl Config { bind_address: "127.0.0.1:0".to_string(), maskinporten: Some(Provider { client_id: "client-id".to_string(), - client_jwk: r#"{"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"}"#.to_string(), + client_jwk: Some(r#"{"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"}"#.to_string()), jwks_uri: format!("{url_base}/maskinporten/jwks"), issuer: format!("{url_base}/maskinporten"), - token_endpoint: format!("{url_base}/maskinporten/token"), + token_endpoint: Some(format!("{url_base}/maskinporten/token")), }), azure_ad: Some(Provider { client_id: "client-id".to_string(), - client_jwk: r#"{"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"}"#.to_string(), + client_jwk: Some(r#"{"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"}"#.to_string()), jwks_uri: format!("{url_base}/azuread/jwks"), issuer: format!("{url_base}/azuread"), - token_endpoint: format!("{url_base}/azuread/token"), + token_endpoint: Some(format!("{url_base}/azuread/token")), }), - token_x:Some( Provider { + token_x: Some( Provider { client_id: "client-id".to_string(), - client_jwk: r#"{"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"}"#.to_string(), + client_jwk: Some(r#"{"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"}"#.to_string()), jwks_uri: format!("{url_base}/tokenx/jwks"), issuer: format!("{url_base}/tokenx"), - token_endpoint: format!("{url_base}/tokenx/token"), + token_endpoint: Some(format!("{url_base}/tokenx/token")), + }), + idporten: Some( Provider { + client_id: "default".to_string(), // expected audience for tokens returned by mock-oauth2-server + client_jwk: None, + jwks_uri: format!("{url_base}/idporten/jwks"), + issuer: format!("{url_base}/idporten"), + token_endpoint: None, }), } } diff --git a/src/grants.rs b/src/grants.rs index d4607c7..b5e8b60 100644 --- a/src/grants.rs +++ b/src/grants.rs @@ -103,3 +103,9 @@ impl TokenRequestBuilder for TokenExchange { }) } } + +impl TokenRequestBuilder for () { + fn token_request(_: TokenRequestBuilderParams) -> Option { + None + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 250629c..3925cb8 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -257,6 +257,16 @@ impl HandlerState { providers.push(provider); } + if let Some(provider_cfg) = &cfg.idporten { + info!("Fetch JWKS for ID-porten..."); + let provider = new::<(), ()>( + IdentityProvider::IDPorten, + provider_cfg, + Some(provider_cfg.client_id.clone()), + ).await?; + providers.push(provider); + } + Ok(Self { cfg, providers, diff --git a/src/identity_provider.rs b/src/identity_provider.rs index 0c5a855..8c822b6 100644 --- a/src/identity_provider.rs +++ b/src/identity_provider.rs @@ -199,6 +199,8 @@ pub enum IdentityProvider { TokenX, #[serde(rename = "maskinporten")] Maskinporten, + #[serde(rename = "idporten")] + IDPorten, } impl Display for IdentityProvider { @@ -267,11 +269,11 @@ impl IntrospectRequest { #[derive(Clone)] pub struct Provider { client_id: String, - pub token_endpoint: String, + pub token_endpoint: Option, identity_provider_kind: IdentityProvider, issuer: String, - private_jwk: jwt::EncodingKey, - client_assertion_header: jwt::Header, + private_jwk: Option, + client_assertion_header: Option, upstream_jwks: jwks::Jwks, _fake_request: PhantomData, _fake_assertion: PhantomData, @@ -286,15 +288,22 @@ where kind: IdentityProvider, issuer: String, client_id: String, - token_endpoint: String, - private_jwk: String, + token_endpoint: Option, + private_jwk: Option, upstream_jwks: jwks::Jwks, ) -> Option { - let client_private_jwk: jwk::JsonWebKey = private_jwk.parse().ok()?; - let alg: jwt::Algorithm = client_private_jwk.algorithm?.into(); - let kid: String = client_private_jwk.key_id.clone()?; - let mut client_assertion_header = jwt::Header::new(alg); - client_assertion_header.kid = Some(kid); + let (client_private_jwk, client_assertion_header) = if let Some(private_jwk) = private_jwk { + let client_private_jwk: jwk::JsonWebKey = private_jwk.parse().ok()?; + let alg: jwt::Algorithm = client_private_jwk.algorithm?.into(); + let kid: String = client_private_jwk.key_id.clone()?; + + let mut header = jwt::Header::new(alg); + header.kid = Some(kid); + + (Some(client_private_jwk.key.to_encoding_key()), Some(header)) + } else { + (None, None) + }; Some(Self { client_id, @@ -303,16 +312,15 @@ where upstream_jwks, issuer, identity_provider_kind: kind, - private_jwk: client_private_jwk.key.to_encoding_key(), + private_jwk: client_private_jwk, _fake_request: Default::default(), _fake_assertion: Default::default(), }) } - fn create_assertion(&self, target: String) -> String { - let assertion = A::new(self.token_endpoint.clone(), self.client_id.clone(), target); - serialize(assertion, &self.client_assertion_header, &self.private_jwk).unwrap() - // FIXME: don't unwrap + fn create_assertion(&self, target: String) -> Option { + let assertion = A::new(self.token_endpoint.as_ref()?.clone(), self.client_id.clone(), target); + serialize(assertion, self.client_assertion_header.as_ref()?, self.private_jwk.as_ref()?).ok() } } @@ -326,7 +334,7 @@ where async fn get_token(&self, request: TokenRequest) -> Result { let token_request = TokenRequestBuilderParams { target: request.target.clone(), - assertion: self.create_assertion(request.target), + assertion: self.create_assertion(request.target).ok_or(ApiError::TokenRequestUnsupported(self.identity_provider_kind.clone()))?, client_id: Some(self.client_id.clone()), user_token: None, }; @@ -336,7 +344,7 @@ where async fn exchange_token(&self, request: TokenExchangeRequest) -> Result { let token_request = TokenRequestBuilderParams { target: request.target.clone(), - assertion: self.create_assertion(request.target), + assertion: self.create_assertion(request.target).ok_or(ApiError::TokenExchangeUnsupported(self.identity_provider_kind.clone()))?, client_id: Some(self.client_id.clone()), user_token: Some(request.user_token), }; @@ -359,7 +367,7 @@ where let client = reqwest::Client::new(); let response = client - .post(self.token_endpoint.clone()) + .post(self.token_endpoint.clone().ok_or(ApiError::TokenRequestUnsupported(self.identity_provider_kind.clone()))?) .header("accept", "application/json") .form(¶ms) .send() @@ -461,7 +469,6 @@ where None => false, } } - } impl ShouldHandler for Provider @@ -480,5 +487,17 @@ where None => false, } } +} +impl ShouldHandler for Provider<(), A> +where + A: Serialize + Assertion, +{ + fn should_handle_introspect_request(&self, request: &IntrospectRequest) -> bool { + match request.issuer() { + Some(iss) if iss == self.issuer => true, + Some(_) => false, + None => false, + } + } }