diff --git a/Cargo.toml b/Cargo.toml index dcba400..afc8fde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,9 @@ serde_json = "1.0" serde_with = "3.3.0" serde_path_to_error = "0.1.14" url = { version = "2.3.1", features = ["serde"] } -openidconnect = { version = "4.0.0-alpha.2", features = ["timing-resistant-secret-traits"] } -oauth2 = "5.0.0-alpha.4" +oauth2 = { version = "5.0.0-alpha.4", features = [ + "timing-resistant-secret-traits", +] } async-signature = "0.3.0" rand = "0.8.5" time = { version = "0.3.29", features = ["serde"] } @@ -28,6 +29,8 @@ thiserror = "1.0.49" tracing = "0.1" base64 = "0.21.4" serde_urlencoded = "0.7.1" +anyhow = "1.0.86" +sha2 = "0.10.8" [dev-dependencies] assert-json-diff = "2.0.2" diff --git a/src/authorization.rs b/src/authorization.rs index a03cd76..eccbe03 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -1,72 +1,70 @@ -use oauth2::PkceCodeChallenge; -use openidconnect::{CsrfToken, IssuerUrl}; +use std::borrow::Cow; + +use oauth2::{CsrfToken, PkceCodeChallenge}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::profiles::AuthorizationDetailsProfile; +use crate::{ + profiles::AuthorizationDetailsProfile, + types::{IssuerState, IssuerUrl, UserHint}, +}; -pub struct AuthorizationRequest<'a, AD> -where - AD: AuthorizationDetailsProfile, -{ - inner: oauth2::AuthorizationRequest<'a>, // TODO - authorization_details: Vec>, - wallet_issuer: Option, // TODO SIOP related - user_hint: Option, - issuer_state: Option, +pub struct AuthorizationRequest<'a> { + inner: oauth2::AuthorizationRequest<'a>, } // TODO 5.1.2 scopes -impl<'a, AD> AuthorizationRequest<'a, AD> -where - AD: AuthorizationDetailsProfile, -{ - pub(crate) fn new( - inner: oauth2::AuthorizationRequest<'a>, - authorization_details: Vec>, - wallet_issuer: Option, - user_hint: Option, - issuer_state: Option, - ) -> Self { - Self { - inner, - authorization_details, - wallet_issuer, - user_hint, - issuer_state, - } +impl<'a> AuthorizationRequest<'a> { + pub(crate) fn new(inner: oauth2::AuthorizationRequest<'a>) -> Self { + Self { inner } } - pub fn url(self) -> Result<(Url, CsrfToken), serde_json::Error> { - let (mut url, token) = self.inner.url(); - url.query_pairs_mut().append_pair( - "authorization_details", - &serde_json::to_string(&self.authorization_details)?, - ); - if let Some(w) = self.wallet_issuer { - url.query_pairs_mut() - .append_pair("wallet_issuer", &w.to_string()); - } - if let Some(h) = self.user_hint { - url.query_pairs_mut().append_pair("user_hint", &h); - } - if let Some(s) = self.issuer_state { - url.query_pairs_mut() - .append_pair("issuer_state", s.secret()); - } - Ok((url, token)) + pub fn url(self) -> (Url, CsrfToken) { + self.inner.url() } pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self { self.inner = self.inner.set_pkce_challenge(pkce_code_challenge); self } - pub fn set_authorization_details( + + pub fn set_authorization_details( mut self, authorization_details: Vec>, - ) -> Self { - self.authorization_details = authorization_details; + ) -> Result { + self.inner = self.inner.add_extra_param( + "authorization_details", + serde_json::to_string(&authorization_details)?, + ); + Ok(self) + } + + pub fn set_issuer_state(mut self, issuer_state: &'a IssuerState) -> Self { + self.inner = self + .inner + .add_extra_param("issuer_state", issuer_state.secret()); + self + } + + pub fn set_user_hint(mut self, user_hint: &'a UserHint) -> Self { + self.inner = self.inner.add_extra_param("user_hint", user_hint.secret()); + self + } + + pub fn set_wallet_issuer(mut self, wallet_issuer: &'a IssuerUrl) -> Self { + self.inner = self + .inner + .add_extra_param("wallet_issuer", wallet_issuer.as_str()); + self + } + + pub fn add_extra_param(mut self, name: N, value: V) -> Self + where + N: Into>, + V: Into>, + { + self.inner = self.inner.add_extra_param(name, value); self } } @@ -97,8 +95,12 @@ mod test { use serde_json::json; use crate::{ - core::profiles::{w3c, CoreProfilesAuthorizationDetails, ValueAuthorizationDetails}, - metadata::CredentialUrl, + core::{ + metadata::CredentialIssuerMetadata, + profiles::{w3c, CoreProfilesAuthorizationDetails, ValueAuthorizationDetails}, + }, + metadata::AuthorizationServerMetadata, + types::CredentialUrl, }; use super::*; @@ -202,14 +204,26 @@ mod test { // Modifed the code_challenge from the example and added state and removed spaces in authorization_details let mut expected_url = Url::try_from("https://server.example.com/authorize?response_type=code&client_id=s6BhdRkqt3&code_challenge=MYdqq2Vt_ZLMAWpXXsjGIrlxrCF2e4ZP4SxDf7cm_tg&code_challenge_method=S256&authorization_details=%5B%7B%22type%22%3A%22openid_credential%22%2C%22format%22%3A%22jwt_vc_json%22%2C%22credential_definition%22%3A%7B%22type%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%7D%5D&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb&state=state").unwrap(); - let client = crate::core::client::Client::new( - ClientId::new("s6BhdRkqt3".to_string()), - IssuerUrl::new("https://server.example.com".into()).unwrap(), + let issuer = IssuerUrl::new("https://server.example.com".into()).unwrap(); + + let credential_issuer_metadata = CredentialIssuerMetadata::new( + issuer.clone(), CredentialUrl::new("https://server.example.com/credential".into()).unwrap(), - AuthUrl::new("https://server.example.com/authorize".into()).unwrap(), - None, + ); + + let authorization_server_metadata = AuthorizationServerMetadata::new( + issuer, TokenUrl::new("https://server.example.com/token".into()).unwrap(), + ) + .set_authorization_endpoint(Some( + AuthUrl::new("https://server.example.com/authorize".into()).unwrap(), + )); + + let client = crate::core::client::Client::from_issuer_metadata( + ClientId::new("s6BhdRkqt3".to_string()), RedirectUrl::new("https://client.example.org/cb".into()).unwrap(), + credential_issuer_metadata, + authorization_server_metadata, ); let pkce_verifier = @@ -230,10 +244,12 @@ mod test { }]; let req = client .authorize_url(move || state) + .unwrap() .set_authorization_details(authorization_details) + .unwrap() .set_pkce_challenge(pkce_challenge); - let (mut url, _) = req.url().unwrap(); + let (mut url, _) = req.url(); let expected_query: HashSet<(String, String)> = expected_url.query_pairs().into_owned().collect(); expected_url.set_query(None); diff --git a/src/client.rs b/src/client.rs index 18aadf3..31fe1ce 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,47 +1,39 @@ use oauth2::{ basic::{BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse}, - AccessToken, AuthUrl, AuthorizationCode, ClientId, CodeTokenRequest, CsrfToken, EndpointNotSet, - EndpointSet, RedirectUrl, StandardRevocableToken, TokenUrl, + AccessToken, AuthUrl, AuthorizationCode, ClientId, CodeTokenRequest, ConfigurationError, + CsrfToken, EndpointMaybeSet, EndpointNotSet, EndpointSet, RedirectUrl, StandardRevocableToken, + TokenUrl, }; -use openidconnect::{ - core::{ - CoreApplicationType, CoreClientAuthMethod, CoreGrantType, CoreJsonWebKey, - CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreResponseType, - CoreSubjectIdentifierType, - }, - registration::ClientMetadata, - IssuerUrl, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, -}; -use serde::{Deserialize, Serialize}; use crate::{ authorization::AuthorizationRequest, credential, credential_response_encryption::CredentialResponseEncryptionMetadata, metadata::{ - AuthorizationMetadata, CredentialIssuerMetadata, CredentialIssuerMetadataDisplay, - CredentialMetadata, CredentialUrl, + credential_issuer::{CredentialIssuerMetadataDisplay, CredentialMetadata}, + AuthorizationServerMetadata, CredentialIssuerMetadata, }, - profiles::{AuthorizationDetailsProfile, Profile}, + profiles::Profile, pushed_authorization::PushedAuthorizationRequest, token, - types::{BatchCredentialUrl, DeferredCredentialUrl, ParUrl}, + types::{BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, IssuerUrl, ParUrl}, }; #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("Pushed authorization request is not supported")] - ParUnsupported(), - - #[error("Batch credential request is not supported")] - BcrUnsupported(), + #[error("Batch Credential Request are not supported by this issuer")] + BcrUnsupported, + #[error("Pushed Authorization Requests are not supported by this issuer")] + ParUnsupported, + #[error("Authorization Requests are not supported by this issuer: {0}")] + AuthUnsupported(ConfigurationError), + #[error("An error occurred when discovering metadata: {0}")] + MetadataDiscovery(anyhow::Error), } -pub struct Client +pub struct Client where C: Profile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { inner: oauth2::Client< BasicErrorResponse, @@ -49,7 +41,7 @@ where BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse, - EndpointSet, + EndpointMaybeSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, @@ -60,139 +52,121 @@ where par_auth_url: Option, batch_credential_endpoint: Option, deferred_credential_endpoint: Option, - credential_response_encryption: Option>, + credential_response_encryption: Option, credential_configurations_supported: Vec>, display: Option>, } -impl Client +impl Client where C: Profile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { - pub fn new( - client_id: ClientId, - issuer: IssuerUrl, - credential_endpoint: CredentialUrl, - auth_url: AuthUrl, - par_auth_url: Option, - token_url: TokenUrl, - redirect_uri: RedirectUrl, - ) -> Self { - let inner = oauth2::Client::new(client_id) - .set_redirect_uri(redirect_uri) - .set_auth_uri(auth_url) - .set_token_uri(token_url); - Self { - inner, - issuer, - credential_endpoint, - par_auth_url, - batch_credential_endpoint: None, - deferred_credential_endpoint: None, - credential_response_encryption: None, - credential_configurations_supported: vec![], - display: None, - } - } - field_getters_setters![ - pub self [self] ["issuer metadata value"] { + pub self [self] ["client configuration value"] { set_issuer -> issuer[IssuerUrl], set_credential_endpoint -> credential_endpoint[CredentialUrl], set_batch_credential_endpoint -> batch_credential_endpoint[Option], set_deferred_credential_endpoint -> deferred_credential_endpoint[Option], - set_credential_response_encryption -> credential_response_encryption[Option>], + set_credential_response_encryption -> credential_response_encryption[Option], set_credential_configurations_supported -> credential_configurations_supported[Vec>], set_display -> display[Option>], } ]; + pub fn from_credential_offer() -> Result { + todo!() + } + + pub async fn from_credential_offer_async() -> Result { + todo!() + } + pub fn from_issuer_metadata( - credential_issuer_metadata: CredentialIssuerMetadata, - authorization_metadata: AuthorizationMetadata, client_id: ClientId, redirect_uri: RedirectUrl, + credential_issuer_metadata: CredentialIssuerMetadata, + authorization_metadata: AuthorizationServerMetadata, ) -> Self { - Self::new( + let inner = Self::new_inner_client( client_id, - credential_issuer_metadata.credential_issuer().clone(), - credential_issuer_metadata.credential_endpoint().clone(), - authorization_metadata.authorization_endpoint().clone(), - authorization_metadata - .pushed_authorization_endpoint() - .clone(), - authorization_metadata.token_endpoint().clone(), redirect_uri, - ) - .set_batch_credential_endpoint( - credential_issuer_metadata + authorization_metadata.authorization_endpoint().cloned(), + authorization_metadata.token_endpoint().clone(), + ); + + Self { + inner, + issuer: credential_issuer_metadata.credential_issuer().clone(), + credential_endpoint: credential_issuer_metadata.credential_endpoint().clone(), + par_auth_url: authorization_metadata + .pushed_authorization_request_endpoint() + .cloned(), + batch_credential_endpoint: credential_issuer_metadata .batch_credential_endpoint() .cloned(), - ) - .set_deferred_credential_endpoint( - credential_issuer_metadata + deferred_credential_endpoint: credential_issuer_metadata .deferred_credential_endpoint() .cloned(), - ) - .set_credential_response_encryption( - credential_issuer_metadata + credential_response_encryption: credential_issuer_metadata .credential_response_encryption() .cloned(), - ) - .set_display(credential_issuer_metadata.display().cloned()) - .set_credential_configurations_supported( - credential_issuer_metadata + credential_configurations_supported: credential_issuer_metadata .credential_configurations_supported() .clone(), - ) + display: credential_issuer_metadata.display().cloned(), + } } - pub fn pushed_authorization_request( + pub fn pushed_authorization_request( &self, state_fn: S, - ) -> Result, Error> + ) -> Result where S: FnOnce() -> CsrfToken, - AD: AuthorizationDetailsProfile, { - if self.par_auth_url.is_none() { - return Err(Error::ParUnsupported()); - } - let inner = self.inner.authorize_url(state_fn); + let Some(par_url) = self.par_auth_url.as_ref() else { + return Err(Error::ParUnsupported); + }; + let inner = self.authorize_url(state_fn)?; Ok(PushedAuthorizationRequest::new( inner, - self.par_auth_url.clone().unwrap(), - self.inner.auth_uri().clone(), - vec![], - None, - None, - None, + par_url.clone(), + self.inner + .auth_uri() + .cloned() + .ok_or(Error::AuthUnsupported(ConfigurationError::MissingUrl( + "authorization", + )))?, )) } - pub fn authorize_url(&self, state_fn: S) -> AuthorizationRequest + pub fn authorize_url(&self, state_fn: S) -> Result where S: FnOnce() -> CsrfToken, - AD: AuthorizationDetailsProfile, { - let inner = self.inner.authorize_url(state_fn); - AuthorizationRequest::new(inner, vec![], None, None, None) + let inner = self + .inner + .authorize_url(state_fn) + .map_err(Error::AuthUnsupported)?; + Ok(AuthorizationRequest::new(inner)) } pub fn exchange_code( &self, code: AuthorizationCode, - ) -> CodeTokenRequest<'_, token::Error, token::Response> { + ) -> CodeTokenRequest<'_, BasicErrorResponse, token::Response> { self.inner.exchange_code(code) } + // pub fn exchange_pre_authorized_code(&self, pre_authorized_code: PreAuthorizedCode) -> () { + // todo!() + // } + pub fn request_credential( &self, access_token: AccessToken, profile_fields: C::Credential, - ) -> credential::RequestBuilder { + ) -> credential::RequestBuilder { let body = credential::Request::new(profile_fields); credential::RequestBuilder::new(body, self.credential_endpoint().clone(), access_token) } @@ -201,16 +175,14 @@ where &self, access_token: AccessToken, profile_fields: Vec, - ) -> Result, Error> { - let endpoint = if let Some(endpoint) = self.batch_credential_endpoint() { - endpoint - } else { - return Err(Error::BcrUnsupported()); + ) -> Result, Error> { + let Some(endpoint) = self.batch_credential_endpoint() else { + return Err(Error::BcrUnsupported); }; let body = credential::BatchRequest::new( profile_fields .into_iter() - .map(|pf| credential::Request::new(pf)) + .map(credential::Request::new) .collect(), ); Ok(credential::BatchRequestBuilder::new( @@ -219,23 +191,27 @@ where access_token, )) } -} -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct AdditionalClientMetadata { - credential_offer_endpoint: Option, + fn new_inner_client( + client_id: ClientId, + redirect_uri: RedirectUrl, + auth_url: Option, + token_url: TokenUrl, + ) -> oauth2::Client< + BasicErrorResponse, + token::Response, + BasicTokenIntrospectionResponse, + StandardRevocableToken, + BasicRevocationErrorResponse, + EndpointMaybeSet, + EndpointNotSet, + EndpointNotSet, + EndpointNotSet, + EndpointSet, + > { + oauth2::Client::new(client_id) + .set_redirect_uri(redirect_uri) + .set_auth_uri_option(auth_url) + .set_token_uri(token_url) + } } - -impl openidconnect::registration::AdditionalClientMetadata for AdditionalClientMetadata {} - -pub type Metadata = ClientMetadata< - AdditionalClientMetadata, - CoreApplicationType, - CoreClientAuthMethod, - CoreGrantType, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, - CoreJsonWebKey, - CoreResponseType, - CoreSubjectIdentifierType, ->; // TODO diff --git a/src/core/mod.rs b/src/core/mod.rs index 01eb862..03a2999 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,36 +1,21 @@ pub mod profiles; pub mod metadata { - use openidconnect::core::{CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm}; - use crate::metadata; use super::profiles::CoreProfilesConfiguration; - pub type CredentialIssuerMetadata = metadata::CredentialIssuerMetadata< - CoreProfilesConfiguration, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, - >; + pub type CredentialIssuerMetadata = + metadata::CredentialIssuerMetadata; } pub mod credential { - use openidconnect::core::{CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm}; - use crate::credential; use super::profiles::CoreProfilesRequest; - pub type Request = credential::Request< - CoreProfilesRequest, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, - >; - pub type BatchRequest = credential::BatchRequest< - CoreProfilesRequest, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, - >; + pub type Request = credential::Request; + pub type BatchRequest = credential::BatchRequest; } pub mod authorization { @@ -43,25 +28,22 @@ pub mod authorization { } pub mod client { - use openidconnect::core::{CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm}; use crate::client; use super::profiles::CoreProfiles; - pub type Client = client::Client< - CoreProfiles, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, - >; + pub type Client = client::Client; } #[cfg(test)] mod test { - use openidconnect::IssuerUrl; use profiles::w3c::{self, CredentialDefinitionLD}; - use crate::{credential_response_encryption::CredentialUrl, metadata::CredentialMetadata}; + use crate::{ + credential_response_encryption::CredentialUrl, + metadata::credential_issuer::CredentialMetadata, types::IssuerUrl, + }; use super::*; @@ -70,13 +52,13 @@ mod test { let metadata = super::metadata::CredentialIssuerMetadata::new( IssuerUrl::from_url("https://example.com".parse().unwrap()), CredentialUrl::from_url("https://example.com/credential".parse().unwrap()), - vec![CredentialMetadata::new( - "credential1".into(), - profiles::CoreProfilesConfiguration::JWTVC(w3c::jwt::Configuration::new( - w3c::CredentialDefinition::new(vec!["type1".into()]), - )), - )], - ); + ) + .set_credential_configurations_supported(vec![CredentialMetadata::new( + "credential1".into(), + profiles::CoreProfilesConfiguration::JWTVC(w3c::jwt::Configuration::new( + w3c::CredentialDefinition::new(vec!["type1".into()]), + )), + )]); serde_json::to_vec(&metadata).unwrap(); } @@ -85,21 +67,21 @@ mod test { let metadata = super::metadata::CredentialIssuerMetadata::new( IssuerUrl::from_url("https://example.com".parse().unwrap()), CredentialUrl::from_url("https://example.com/credential".parse().unwrap()), - vec![CredentialMetadata::new( - "credential1".into(), - profiles::CoreProfilesConfiguration::LDVC(w3c::ldp::Configuration::new( + ) + .set_credential_configurations_supported(vec![CredentialMetadata::new( + "credential1".into(), + profiles::CoreProfilesConfiguration::LDVC(w3c::ldp::Configuration::new( + vec![serde_json::Value::String( + "http://example.com/context".into(), + )], + CredentialDefinitionLD::new( + w3c::CredentialDefinition::new(vec!["type1".into()]), vec![serde_json::Value::String( "http://example.com/context".into(), )], - CredentialDefinitionLD::new( - w3c::CredentialDefinition::new(vec!["type1".into()]), - vec![serde_json::Value::String( - "http://example.com/context".into(), - )], - ), - )), - )], - ); + ), + )), + )]); serde_json::to_vec(&metadata).unwrap(); } } diff --git a/src/core/profiles/w3c/mod.rs b/src/core/profiles/w3c/mod.rs index 399ca94..e1c69ce 100644 --- a/src/core/profiles/w3c/mod.rs +++ b/src/core/profiles/w3c/mod.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use crate::metadata::CredentialIssuerMetadataDisplay; +use crate::metadata::credential_issuer::CredentialIssuerMetadataDisplay; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/src/credential.rs b/src/credential.rs index c0a3723..1342c26 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -6,42 +6,33 @@ use oauth2::{ header::{ACCEPT, CONTENT_TYPE}, HeaderValue, Method, StatusCode, }, - AccessToken, AsyncHttpClient, HttpRequest, HttpResponse, StandardErrorResponse, SyncHttpClient, -}; -use openidconnect::{ - ClaimsVerificationError, ErrorResponseType, JweContentEncryptionAlgorithm, - JweKeyManagementAlgorithm, Nonce, + AccessToken, AsyncHttpClient, ErrorResponseType, HttpRequest, HttpResponse, + StandardErrorResponse, SyncHttpClient, }; use serde::{Deserialize, Serialize}; use crate::{ credential_response_encryption::CredentialResponseEncryption, http_utils::{auth_bearer, content_type_has_essence, MIME_TYPE_JSON}, - metadata::CredentialUrl, profiles::{CredentialRequestProfile, CredentialResponseProfile}, proof_of_possession::Proof, - types::BatchCredentialUrl, + types::{BatchCredentialUrl, CredentialUrl, Nonce}, }; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct Request +pub struct Request where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { #[serde(flatten, bound = "CR: CredentialRequestProfile")] additional_profile_fields: CR, proof: Option, - #[serde(bound = "JA: JweKeyManagementAlgorithm, JE: JweContentEncryptionAlgorithm")] - credential_response_encryption: Option>, + credential_response_encryption: Option, } -impl Request +impl Request where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { pub(crate) fn new(additional_profile_fields: CR) -> Self { Self { @@ -55,33 +46,25 @@ where pub self [self] ["credential request value"] { set_additional_profile_fields -> additional_profile_fields[CR], set_proof -> proof[Option], - set_credential_response_encryption -> credential_response_encryption[Option>], + set_credential_response_encryption -> credential_response_encryption[Option], } ]; } -pub struct RequestBuilder +pub struct RequestBuilder where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { - body: Request, + body: Request, url: CredentialUrl, access_token: AccessToken, } -impl RequestBuilder +impl RequestBuilder where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { - pub(crate) fn new( - body: Request, - url: CredentialUrl, - access_token: AccessToken, - ) -> Self { + pub(crate) fn new(body: Request, url: CredentialUrl, access_token: AccessToken) -> Self { Self { body, url, @@ -93,7 +76,7 @@ where pub self [self.body] ["credential request value"] { set_additional_profile_fields -> additional_profile_fields[CR], set_proof -> proof[Option], - set_credential_response_encryption -> credential_response_encryption[Option>], + set_credential_response_encryption -> credential_response_encryption[Option], } ]; @@ -183,25 +166,21 @@ where } } -pub struct BatchRequestBuilder +pub struct BatchRequestBuilder where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { - body: BatchRequest, + body: BatchRequest, url: BatchCredentialUrl, access_token: AccessToken, } -impl BatchRequestBuilder +impl BatchRequestBuilder where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { pub(crate) fn new( - body: BatchRequest, + body: BatchRequest, url: BatchCredentialUrl, access_token: AccessToken, ) -> Self { @@ -334,8 +313,6 @@ pub enum RequestError where RE: std::error::Error + 'static, { - #[error("Failed to verify claims")] - ClaimsVerification(#[source] ClaimsVerificationError), #[error("Failed to parse server response")] Parse(#[source] serde_path_to_error::Error), #[error("Request failed")] @@ -346,7 +323,7 @@ where Other(String), } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Deserialize, PartialEq, Serialize)] pub struct Response where CR: CredentialResponseProfile, @@ -404,23 +381,19 @@ impl ErrorResponseType for ErrorType {} pub type Error = StandardErrorResponse; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct BatchRequest +pub struct BatchRequest where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { #[serde(bound = "CR: CredentialRequestProfile")] - credential_requests: Vec>, + credential_requests: Vec>, } -impl BatchRequest +impl BatchRequest where CR: CredentialRequestProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { - pub fn new(credential_requests: Vec>) -> Self { + pub fn new(credential_requests: Vec>) -> Self { Self { credential_requests, } diff --git a/src/credential_offer.rs b/src/credential_offer.rs index 947382e..48ace2e 100644 --- a/src/credential_offer.rs +++ b/src/credential_offer.rs @@ -1,11 +1,14 @@ #![allow(clippy::large_enum_variant)] -use openidconnect::{CsrfToken, IssuerUrl, Scope}; +use oauth2::Scope; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none}; use url::Url; -use crate::profiles::CredentialOfferProfile; +use crate::{ + profiles::CredentialOfferProfile, + types::{IssuerState, IssuerUrl, PreAuthorizedCode}, +}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(untagged)] @@ -82,11 +85,11 @@ impl CredentialOfferGrants { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct AuthorizationCodeGrant { - issuer_state: Option, + issuer_state: Option, authorization_server: Option, } impl AuthorizationCodeGrant { - pub fn new(issuer_state: Option, authorization_server: Option) -> Self { + pub fn new(issuer_state: Option, authorization_server: Option) -> Self { Self { issuer_state, authorization_server, @@ -94,7 +97,7 @@ impl AuthorizationCodeGrant { } field_getters_setters![ pub self [self] ["authorization code grants"] { - set_issuer_state -> issuer_state[Option], + set_issuer_state -> issuer_state[Option], set_authorization_server -> authorization_server[Option], } ]; @@ -103,13 +106,14 @@ impl AuthorizationCodeGrant { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PreAuthorizationCodeGrant { #[serde(rename = "pre-authorized_code")] - pre_authorized_code: String, - tx_code: Option, + pre_authorized_code: PreAuthorizedCode, + tx_code: Option, interval: Option, authorization_server: Option, } + impl PreAuthorizationCodeGrant { - pub fn new(pre_authorized_code: String) -> Self { + pub fn new(pre_authorized_code: PreAuthorizedCode) -> Self { Self { pre_authorized_code, tx_code: None, @@ -119,8 +123,8 @@ impl PreAuthorizationCodeGrant { } field_getters_setters![ pub self [self] ["pre-authorized_code grants"] { - set_pre_authorized_code -> pre_authorized_code[String], - set_tx_code -> tx_code[Option], + set_pre_authorized_code -> pre_authorized_code[PreAuthorizedCode], + set_tx_code -> tx_code[Option], set_interval -> interval[Option], set_authorization_server -> authorization_server[Option], } @@ -144,12 +148,13 @@ impl Default for InputMode { #[serde_as] #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct TxCode { +pub struct TxCodeDefinition { input_mode: Option, length: Option, description: Option, } -impl TxCode { + +impl TxCodeDefinition { pub fn new( input_mode: Option, length: Option, diff --git a/src/credential_response_encryption.rs b/src/credential_response_encryption.rs index 6224a96..2e6906a 100644 --- a/src/credential_response_encryption.rs +++ b/src/credential_response_encryption.rs @@ -1,38 +1,30 @@ -#![allow(clippy::type_complexity)] - -use openidconnect::{JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm}; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, skip_serializing_none}; use ssi_jwk::JWK; pub use crate::types::{BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, ParUrl}; -#[serde_as] -#[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct CredentialResponseEncryptionMetadata -where - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, -{ - #[serde(bound = "JA: JweKeyManagementAlgorithm")] - alg_values_supported: Vec, - #[serde(bound = "JE: JweContentEncryptionAlgorithm")] - enc_values_supported: Vec, +pub struct CredentialResponseEncryptionMetadata { + alg_values_supported: Vec, + enc_values_supported: Vec, encryption_required: bool, } -#[serde_as] -#[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct CredentialResponseEncryption -where - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, -{ +pub struct CredentialResponseEncryption { jwk: JWK, - #[serde(bound = "JA: JweKeyManagementAlgorithm")] - alg: JA, - #[serde(bound = "JE: JweContentEncryptionAlgorithm")] - enc: JE, + alg: Alg, + enc: Enc, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum Alg { + #[serde(untagged)] + Other(String), +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum Enc { + #[serde(untagged)] + Other(String), } diff --git a/src/http_utils.rs b/src/http_utils.rs index 96f8bf0..ff80a58 100644 --- a/src/http_utils.rs +++ b/src/http_utils.rs @@ -1,3 +1,4 @@ +use anyhow::{bail, Result}; use oauth2::{ http::{ header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE}, @@ -24,7 +25,7 @@ pub fn content_type_has_essence(content_type: &HeaderValue, expected_essence: &s .is_some() } -pub fn check_content_type(headers: &HeaderMap, expected_content_type: &str) -> Result<(), String> { +pub fn check_content_type(headers: &HeaderMap, expected_content_type: &str) -> Result<()> { headers .get(CONTENT_TYPE) .map_or(Ok(()), |content_type| @@ -32,13 +33,11 @@ pub fn check_content_type(headers: &HeaderMap, expected_content_type: &str) -> R // may be followed by optional whitespace and/or a parameter (e.g., charset). // See https://tools.ietf.org/html/rfc7231#section-3.1.1.1. if !content_type_has_essence(content_type, expected_content_type) { - Err( - format!( + bail!( "Unexpected response Content-Type: {:?}, should be `{}`", content_type, expected_content_type ) - ) } else { Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index e900bdf..4970454 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,4 +17,4 @@ pub mod pushed_authorization; pub mod token; mod types; -pub use openidconnect; +pub use oauth2; diff --git a/src/metadata/authorization_server.rs b/src/metadata/authorization_server.rs new file mode 100644 index 0000000..e51bdf3 --- /dev/null +++ b/src/metadata/authorization_server.rs @@ -0,0 +1,155 @@ +use anyhow::{bail, Result}; +use oauth2::{ + AuthUrl, IntrospectionUrl, PkceCodeChallengeMethod, ResponseType, RevocationUrl, Scope, + TokenUrl, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value as Json}; + +use crate::types::{IssuerUrl, JsonWebKeySetUrl, ParUrl, RegistrationUrl, ResponseMode}; + +use super::MetadataDiscovery; + +/// Authorization Server Metadata according to +/// [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414) with the following modifications: +/// * new metadata parameter `pre-authorized_grant_anonymous_access_supported` (as per OID4VP); +/// * `response_types_supported` is now optional (as per OID4VP); +/// * `token_endpoint` is no longer optional (OID4VP cannot be performed without the token +/// endpoint); +/// * additional parameters from +/// [OAuth 2.0 Pushed Authorization Requests](https://datatracker.ietf.org/doc/html/rfc9126). +/// * the following parameters from RFC 8414 are not yet implemented, but may still be accessed via +/// `additional_fields`: +/// * `token_endpoint_auth_methods_supported` +/// * `token_endpoint_auth_signing_alg_values_supported` +/// * `service_documentation` +/// * `ui_locales_supported` +/// * `op_policy_uri` +/// * `op_tos_uri` +/// * `revocation_endpoint_auth_methods_supported` +/// * `revocation_endpoint_auth_singing_alg_values_supported` +/// * `introspection_endpoint_auth_methods_supported` +/// * `introspection_endpoint_auth_singing_alg_values_supported` +/// +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct AuthorizationServerMetadata { + issuer: IssuerUrl, + authorization_endpoint: Option, + token_endpoint: TokenUrl, + jwks_uri: Option, + registration_endpoint: Option, + scopes_supported: Option>, + response_types_supported: Option>, + #[serde(default)] + response_modes_supported: ResponseModes, + #[serde(default)] + grant_types_supported: GrantTypesSupported, + revocation_endpoint: Option, + introspection_endpoint: Option, + code_challenge_methods_supported: Option>, + #[serde(default, rename = "pre-authorized_grant_anonymous_access_supported")] + pre_authorized_grant_anonymous_access_supported: bool, + pushed_authorization_request_endpoint: Option, + #[serde(default)] + require_pushed_authorization_requests: bool, + additional_fields: Map, +} + +impl AuthorizationServerMetadata { + #[cfg(test)] + pub fn new(issuer: IssuerUrl, token_endpoint: TokenUrl) -> Self { + Self { + issuer, + authorization_endpoint: Default::default(), + token_endpoint, + jwks_uri: Default::default(), + registration_endpoint: Default::default(), + scopes_supported: Default::default(), + response_types_supported: Default::default(), + response_modes_supported: Default::default(), + grant_types_supported: Default::default(), + revocation_endpoint: Default::default(), + introspection_endpoint: Default::default(), + code_challenge_methods_supported: Default::default(), + pre_authorized_grant_anonymous_access_supported: false, + pushed_authorization_request_endpoint: Default::default(), + require_pushed_authorization_requests: Default::default(), + additional_fields: Default::default(), + } + } + + field_getters_setters![ + pub self [self] ["authorization server metadata value"] { + set_issuer -> issuer[IssuerUrl], + set_authorization_endpoint -> authorization_endpoint[Option], + set_token_endpoint -> token_endpoint[TokenUrl], + set_jwks_uri -> jwks_uri[Option], + set_registration_endpoint -> registration_endpoint[Option], + set_scopes_supported -> scopes_supported[Option>], + set_response_types_supported -> response_types_supported[Option>], + set_response_modes_supported -> response_modes_supported[ResponseModes], + set_grant_types_supported -> grant_types_supported[GrantTypesSupported], + set_revocation_endpoint -> revocation_endpoint[Option], + set_introspection_endpoint -> introspection_endpoint[Option], + set_code_challenge_methods_supported -> code_challenge_methods_supported[Option>], + set_pre_authorized_grant_anonymous_access_supported -> pre_authorized_grant_anonymous_access_supported[bool], + set_pushed_authorization_request_endpoint -> pushed_authorization_request_endpoint[Option], + set_require_pushed_authorization_requests -> require_pushed_authorization_requests[bool], + } + ]; + + pub fn additional_fields(&self) -> &Map { + &self.additional_fields + } + + pub fn additional_fields_mut(&mut self) -> &mut Map { + &mut self.additional_fields + } +} + +impl MetadataDiscovery for AuthorizationServerMetadata { + const METADATA_URL_SUFFIX: &'static str = ".well-known/oauth-authorization-server"; + + fn validate(&self, issuer: &IssuerUrl) -> Result<()> { + if self.issuer() != issuer { + bail!( + "unexpected issuer URI `{}` (expected `{}`)", + self.issuer().as_str(), + issuer.as_str() + ) + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ResponseModes(pub Vec); + +impl Default for ResponseModes { + fn default() -> Self { + Self(vec![ + ResponseMode::new("query".to_owned()), + ResponseMode::new("fragment".to_owned()), + ]) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct GrantTypesSupported(pub Vec); + +impl Default for GrantTypesSupported { + fn default() -> Self { + Self(vec![GrantType::AuthorizationCode, GrantType::Implicit]) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum GrantType { + AuthorizationCode, + Implicit, + #[serde(rename = "urn:ietf:params:oauth:grant-type:pre-authorized_code")] + PreAuthorizedCode, + #[serde(untagged)] + Extension(String), +} diff --git a/src/metadata.rs b/src/metadata/credential_issuer.rs similarity index 52% rename from src/metadata.rs rename to src/metadata/credential_issuer.rs index ad5480a..0f24ded 100644 --- a/src/metadata.rs +++ b/src/metadata/credential_issuer.rs @@ -1,56 +1,34 @@ -#![allow(clippy::type_complexity)] - -use oauth2::{ - http::{self, header::ACCEPT, HeaderValue, Method, StatusCode}, - AsyncHttpClient, AuthUrl, HttpRequest, HttpResponse, SyncHttpClient, TokenUrl, -}; -use openidconnect::{ - core::{ - CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, - CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, - CoreJwsSigningAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, - }, - AdditionalProviderMetadata, DiscoveryError, IssuerUrl, JsonWebKeySetUrl, - JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, LanguageTag, LogoUrl, - ProviderMetadata, ResponseTypes, Scope, -}; +use anyhow::bail; +use oauth2::Scope; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, KeyValueMap}; -use std::future::Future; -use tracing::{debug, info, warn}; use crate::{ credential_response_encryption::CredentialResponseEncryptionMetadata, - http_utils::{check_content_type, MIME_TYPE_JSON}, profiles::CredentialConfigurationProfile, proof_of_possession::KeyProofTypesSupported, - types::ImageUrl, -}; - -pub use crate::types::{ - BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, NotificationUrl, ParUrl, + types::{ + BatchCredentialUrl, CredentialUrl, DeferredCredentialUrl, IssuerUrl, LanguageTag, LogoUri, + NotificationUrl, + }, }; -const METADATA_URL_SUFFIX: &str = ".well-known/openid-credential-issuer"; -const AUTHORIZATION_METADATA_URL_SUFFIX: &str = ".well-known/oauth-authorization-server"; +use super::MetadataDiscovery; #[serde_as] #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct CredentialIssuerMetadata +pub struct CredentialIssuerMetadata where CM: CredentialConfigurationProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { credential_issuer: IssuerUrl, - authorization_servers: Option>, // Not sure this is the right type + authorization_servers: Option>, credential_endpoint: CredentialUrl, batch_credential_endpoint: Option, deferred_credential_endpoint: Option, notification_endpoint: Option, - #[serde(bound = "JA: JweKeyManagementAlgorithm, JE: JweContentEncryptionAlgorithm")] - credential_response_encryption: Option>, + credential_response_encryption: Option, credential_identifiers_supported: Option, signed_metadata: Option, display: Option>, @@ -59,17 +37,29 @@ where credential_configurations_supported: Vec>, } -impl CredentialIssuerMetadata +impl MetadataDiscovery for CredentialIssuerMetadata where CM: CredentialConfigurationProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, { - pub fn new( - credential_issuer: IssuerUrl, - credential_endpoint: CredentialUrl, - credential_configurations_supported: Vec>, - ) -> Self { + const METADATA_URL_SUFFIX: &'static str = ".well-known/openid-credential-issuer"; + + fn validate(&self, issuer: &IssuerUrl) -> anyhow::Result<()> { + if self.credential_issuer() != issuer { + bail!( + "unexpected issuer URI `{}` (expected `{}`)", + self.credential_issuer().as_str(), + issuer.as_str() + ) + } + Ok(()) + } +} + +impl CredentialIssuerMetadata +where + CM: CredentialConfigurationProfile, +{ + pub fn new(credential_issuer: IssuerUrl, credential_endpoint: CredentialUrl) -> Self { Self { credential_issuer, authorization_servers: None, @@ -81,128 +71,25 @@ where credential_identifiers_supported: None, signed_metadata: None, display: None, - credential_configurations_supported, + credential_configurations_supported: vec![], } } field_getters_setters![ - pub self [self] ["issuer metadata value"] { + pub self [self] ["credential issuer metadata value"] { set_credential_issuer -> credential_issuer[IssuerUrl], set_authorization_servers -> authorization_servers[Option>], set_credential_endpoint -> credential_endpoint[CredentialUrl], set_batch_credential_endpoint -> batch_credential_endpoint[Option], set_deferred_credential_endpoint -> deferred_credential_endpoint[Option], set_notification_endpoint -> notification_endpoint[Option], - set_credential_response_encryption -> credential_response_encryption[Option>], + set_credential_response_encryption -> credential_response_encryption[Option], set_credential_identifiers_supported -> credential_identifiers_supported[Option], set_signed_metadata -> signed_metadata[Option], set_display -> display[Option>], set_credential_configurations_supported -> credential_configurations_supported[Vec>], } ]; - - pub fn discover( - issuer_url: &IssuerUrl, - http_client: &C, - ) -> Result::Error>> - where - C: SyncHttpClient, - { - let discovery_url = issuer_url - .join(METADATA_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; - - http_client - .call( - Self::discovery_request(discovery_url.clone()).map_err(|err| { - DiscoveryError::Other(format!("failed to prepare request: {err}")) - })?, - ) - .map_err(DiscoveryError::Request) - .and_then(|http_response| { - Self::discovery_response(issuer_url, &discovery_url, http_response) - }) - } - - pub fn discover_async<'c, C>( - issuer_url: IssuerUrl, - http_client: &'c C, - ) -> impl Future>::Error>>> + 'c - where - Self: 'c, - C: AsyncHttpClient<'c>, - { - Box::pin(async move { - let discovery_url = issuer_url - .join(METADATA_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; - - let provider_metadata = http_client - .call( - Self::discovery_request(discovery_url.clone()).map_err(|err| { - DiscoveryError::Other(format!("failed to prepare request: {err}")) - })?, - ) - .await - .map_err(DiscoveryError::Request) - .and_then(|http_response| { - Self::discovery_response(&issuer_url, &discovery_url, http_response) - })?; - Ok(provider_metadata) - }) - } - - fn discovery_request(discovery_url: url::Url) -> Result { - http::Request::builder() - .uri(discovery_url.to_string()) - .method(Method::GET) - .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) - .body(Vec::new()) - } - - fn discovery_response( - issuer_url: &IssuerUrl, - discovery_url: &url::Url, - discovery_response: HttpResponse, - ) -> Result> - where - RE: std::error::Error + 'static, - { - if discovery_response.status() != StatusCode::OK { - return Err(DiscoveryError::Response( - discovery_response.status(), - discovery_response.body().to_owned(), - format!( - "HTTP status code {} at {}", - discovery_response.status(), - discovery_url - ), - )); - } - - check_content_type(discovery_response.headers(), MIME_TYPE_JSON).map_err(|err_msg| { - DiscoveryError::Response( - discovery_response.status(), - discovery_response.body().to_owned(), - err_msg, - ) - })?; - - let provider_metadata = serde_path_to_error::deserialize::<_, Self>( - &mut serde_json::Deserializer::from_slice(discovery_response.body()), - ) - .map_err(DiscoveryError::Parse)?; - - if provider_metadata.credential_issuer() != issuer_url { - Err(DiscoveryError::Validation(format!( - "unexpected issuer URI `{}` (expected `{}`)", - provider_metadata.credential_issuer().as_str(), - issuer_url.as_str() - ))) - } else { - Ok(provider_metadata) - } - } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -232,18 +119,18 @@ impl CredentialIssuerMetadataDisplay { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct MetadataDisplayLogo { - url: LogoUrl, + uri: LogoUri, alt_text: Option, } impl MetadataDisplayLogo { - pub fn new(url: LogoUrl, alt_text: Option) -> Self { - Self { url, alt_text } + pub fn new(uri: LogoUri, alt_text: Option) -> Self { + Self { uri, alt_text } } field_getters_setters![ pub self [self] ["metadata display logo value"] { - set_url -> url[LogoUrl], + set_url -> uri[LogoUri], set_alt_text -> alt_text[Option], } ]; @@ -268,17 +155,6 @@ where additional_fields: CM, } -// #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -// pub enum CredentialMetadataProfileWrapper -// where -// CM: CredentialMetadataProfile, -// { -// #[serde(flatten, bound = "CM: CredentialMetadataProfile")] -// Known(CM), -// #[serde(other)] -// Unknown, -// } - impl CredentialMetadata where CM: CredentialConfigurationProfile, @@ -368,304 +244,21 @@ impl CredentialMetadataDisplay { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct MetadataBackgroundImage { - uri: ImageUrl, + uri: LogoUri, } impl MetadataBackgroundImage { - pub fn new(uri: ImageUrl) -> Self { + pub fn new(uri: LogoUri) -> Self { Self { uri } } field_getters_setters![ pub self [self] ["metadata background image value"] { - set_uri -> uri[ImageUrl], + set_uri -> uri[LogoUri], } ]; } -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct AdditionalOAuthMetadata { - #[serde(rename = "pre-authorized_grant_anonymous_access_supported")] - pre_authorized_grant_anonymous_access_supported: Option, - pushed_authorization_request_endpoint: Option, - require_pushed_authorization_requests: Option, -} - -impl AdditionalOAuthMetadata { - pub fn set_pushed_authorization_request_endpoint( - mut self, - pushed_authorization_request_endpoint: Option, - ) -> Self { - self.pushed_authorization_request_endpoint = pushed_authorization_request_endpoint; - self - } - - pub fn set_require_pushed_authorization_requests( - mut self, - require_pushed_authorization_requests: Option, - ) -> Self { - self.require_pushed_authorization_requests = require_pushed_authorization_requests; - self - } -} - -impl AdditionalProviderMetadata for AdditionalOAuthMetadata {} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct AuthorizationMetadata( - ProviderMetadata< - AdditionalOAuthMetadata, - CoreAuthDisplay, - CoreClientAuthMethod, - CoreClaimName, - CoreClaimType, - CoreGrantType, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, - CoreJsonWebKey, - CoreResponseMode, - CoreResponseType, - CoreSubjectIdentifierType, - >, -); // TODO, does a oid4vci specific authorization server need a JWKs, and signed JWTs (instead of just JWE), etc? - -impl AuthorizationMetadata { - #[allow(clippy::too_many_arguments)] - pub fn new( - issuer: IssuerUrl, - authorization_endpoint: AuthUrl, - token_endpoint: TokenUrl, - jwks_uri: JsonWebKeySetUrl, - response_types_supported: Vec>, - subject_types_supported: Vec, - id_token_signing_alg_values_supported: Vec, - additional_metadata: AdditionalOAuthMetadata, - ) -> Self { - Self( - ProviderMetadata::new( - issuer, - authorization_endpoint, - jwks_uri, - response_types_supported, - subject_types_supported, - id_token_signing_alg_values_supported, - additional_metadata, - ) - .set_token_endpoint(Some(token_endpoint)), - ) - } - - fn discover_inner( - issuer_url: &IssuerUrl, - http_client: &C, - ) -> Result::Error>> - where - C: SyncHttpClient, - { - debug!("Discovering {issuer_url}"); - let discovery_url = issuer_url - .join(AUTHORIZATION_METADATA_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; - - http_client - .call( - Self::discovery_request(discovery_url.clone()).map_err(|err| { - DiscoveryError::Other(format!("failed to prepare request: {err}")) - })?, - ) - .map_err(DiscoveryError::Request) - .and_then(|http_response| { - Self::discovery_response(issuer_url, &discovery_url, http_response) - }) - } - - pub fn discover( - credential_issuer_metadata: &CredentialIssuerMetadata, - grant_type: Option, - http_client: &C, - ) -> Result::Error>> - where - C: SyncHttpClient, - CM: CredentialConfigurationProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, - { - if let Some(ref servers) = credential_issuer_metadata.authorization_servers { - for auth_server in servers { - let response = Self::discover_inner(auth_server, http_client); - match (response, grant_type.clone()) { - (Ok(response), Some(grant_type)) => { - let gts = match response.0.grant_types_supported() { - Some(gts) => gts, - None => { - // https://openid.net/specs/openid-connect-discovery-1_0.html - // If omitted, the default value is ["authorization_code", "implicit"]. - &vec![CoreGrantType::AuthorizationCode, CoreGrantType::Implicit] - } - }; - if gts.iter().any(|gt| *gt == grant_type) { - return Ok(response.clone()); - } else { - info!("Auth server not supporting grant type, trying the next one"); - } - } - (Ok(response), None) => { - return Ok(response.clone()); - } - (Err(e), _) => { - warn!("Error fetching auth server metadata, trying the next one: {e:?}"); - } - } - } - } - Self::discover_inner(&credential_issuer_metadata.credential_issuer, http_client) - } - - fn discover_async_inner<'c, C>( - issuer_url: &'c IssuerUrl, - http_client: &'c C, - ) -> impl Future>::Error>>> + 'c - where - Self: 'c, - C: AsyncHttpClient<'c>, - { - Box::pin(async move { - debug!("Discovering {issuer_url}"); - let discovery_url = issuer_url - .join(AUTHORIZATION_METADATA_URL_SUFFIX) - .map_err(DiscoveryError::UrlParse)?; - - http_client - .call( - Self::discovery_request(discovery_url.clone()).map_err(|err| { - DiscoveryError::Other(format!("failed to prepare request: {err}")) - })?, - ) - .await - .map_err(DiscoveryError::Request) - .and_then(|http_response| { - Self::discovery_response(issuer_url, &discovery_url, http_response) - }) - }) - } - - pub fn discover_async<'c, C, CM, JE, JA>( - credential_issuer_metadata: &'c CredentialIssuerMetadata, - grant_type: Option, - http_client: &'c C, - ) -> impl Future>::Error>>> + 'c - where - Self: 'c, - C: AsyncHttpClient<'c>, - CM: CredentialConfigurationProfile, - JE: JweContentEncryptionAlgorithm, - JA: JweKeyManagementAlgorithm + Clone, - { - Box::pin(async move { - if let Some(ref servers) = credential_issuer_metadata.authorization_servers { - for auth_server in servers { - let response = Self::discover_async_inner(auth_server, http_client).await; - match (response, grant_type.clone()) { - (Ok(response), Some(grant_type)) => { - let gts = match response.0.grant_types_supported() { - Some(gts) => gts, - None => { - // https://openid.net/specs/openid-connect-discovery-1_0.html - // If omitted, the default value is ["authorization_code", "implicit"]. - &vec![CoreGrantType::AuthorizationCode, CoreGrantType::Implicit] - } - }; - if gts.iter().any(|gt| *gt == grant_type) { - return Ok(response.clone()); - } else { - info!("Auth server not supporting grant type, trying the next one"); - } - } - (Ok(response), None) => { - return Ok(response.clone()); - } - (Err(e), _) => { - warn!( - "Error fetching auth server metadata, trying the next one: {e:?}" - ); - } - } - } - } - Self::discover_async_inner(&credential_issuer_metadata.credential_issuer, http_client) - .await - }) - } - - fn discovery_request(discovery_url: url::Url) -> Result { - http::Request::builder() - .uri(discovery_url.to_string()) - .method(Method::GET) - .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) - .body(Vec::new()) - } - - fn discovery_response( - issuer_url: &IssuerUrl, - discovery_url: &url::Url, - discovery_response: HttpResponse, - ) -> Result> - where - RE: std::error::Error + 'static, - { - if discovery_response.status() != StatusCode::OK { - return Err(DiscoveryError::Response( - discovery_response.status(), - discovery_response.body().to_owned(), - format!( - "HTTP status code {} at {}", - discovery_response.status(), - discovery_url - ), - )); - } - - check_content_type(discovery_response.headers(), MIME_TYPE_JSON).map_err(|err_msg| { - DiscoveryError::Response( - discovery_response.status(), - discovery_response.body().to_owned(), - err_msg, - ) - })?; - - let provider_metadata = serde_path_to_error::deserialize::<_, Self>( - &mut serde_json::Deserializer::from_slice(discovery_response.body()), - ) - .map_err(DiscoveryError::Parse)?; - - if provider_metadata.0.issuer() != issuer_url { - Err(DiscoveryError::Validation(format!( - "unexpected issuer URI `{}` (expected `{}`)", - provider_metadata.0.issuer().as_str(), - issuer_url.as_str() - ))) - } else { - Ok(provider_metadata) - } - } - - pub fn authorization_endpoint(&self) -> &AuthUrl { - self.0.authorization_endpoint() - } - - pub fn pushed_authorization_endpoint(&self) -> Option { - self.0 - .additional_metadata() - .clone() - .pushed_authorization_request_endpoint - } - - pub fn token_endpoint(&self) -> &TokenUrl { - // TODO find better way to avoid unwrap - self.0.token_endpoint().unwrap() - } -} - #[cfg(test)] mod test { use crate::core::profiles::CoreProfilesConfiguration; @@ -677,8 +270,6 @@ mod test { fn example_credential_issuer_metadata() { let _: CredentialIssuerMetadata< CoreProfilesConfiguration, - CoreJweContentEncryptionAlgorithm, - CoreJweKeyManagementAlgorithm, > = serde_json::from_value(json!({ "credential_issuer": "https://credential-issuer.example.com", "authorization_servers": [ "https://server.example.com" ], @@ -758,7 +349,7 @@ mod test { "name": "University Credential", "locale": "en-US", "logo": { - "url": "https://university.example.edu/public/logo.png", + "uri": "https://university.example.edu/public/logo.png", "alt_text": "a square logo of a university" }, "background_color": "#12107c", @@ -829,7 +420,7 @@ mod test { "name": "University Credential", "locale": "en-US", "logo": { - "url": "https://exampleuniversity.com/public/logo.png", + "uri": "https://exampleuniversity.com/public/logo.png", "alt_text": "a square logo of a university" }, "background_color": "#12107c", @@ -903,7 +494,7 @@ mod test { "name": "University Credential", "locale": "en-US", "logo": { - "url": "https://exampleuniversity.com/public/logo.png", + "uri": "https://exampleuniversity.com/public/logo.png", "alt_text": "a square logo of a university" }, "background_color": "#12107c", @@ -934,7 +525,7 @@ mod test { "name": "Mobile Driving License", "locale": "en-US", "logo": { - "url": "https://examplestate.com/public/mdl.png", + "uri": "https://examplestate.com/public/mdl.png", "alt_text": "a square figure of a mobile driving license" }, "background_color": "#12107c", @@ -947,7 +538,7 @@ mod test { "name": "在籍証明書", "locale": "ja-JP", "logo": { - "url": "https://examplestate.com/public/mdl.png", + "uri": "https://examplestate.com/public/mdl.png", "alt_text": "大学のロゴ" }, "background_color": "#12107c", diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs new file mode 100644 index 0000000..dfb200e --- /dev/null +++ b/src/metadata/mod.rs @@ -0,0 +1,100 @@ +#![allow(clippy::type_complexity)] + +use std::future::Future; + +use anyhow::{bail, Context, Result}; +use oauth2::{ + http::{self, header::ACCEPT, HeaderValue, Method, StatusCode}, + AsyncHttpClient, HttpRequest, HttpResponse, SyncHttpClient, +}; +use serde::{de::DeserializeOwned, Serialize}; +use url::Url; + +use crate::{ + http_utils::{check_content_type, MIME_TYPE_JSON}, + types::IssuerUrl, +}; + +pub mod authorization_server; +pub mod credential_issuer; + +pub use authorization_server::AuthorizationServerMetadata; +pub use credential_issuer::CredentialIssuerMetadata; + +pub trait MetadataDiscovery: DeserializeOwned + Serialize { + const METADATA_URL_SUFFIX: &'static str; + + fn validate(&self, issuer: &IssuerUrl) -> Result<()>; + + fn discover(issuer: &IssuerUrl, http_client: &C) -> Result + where + C: SyncHttpClient, + C::Error: Send + Sync, + { + let discovery_url = discovery_url::(issuer)?; + + let discovery_request = discovery_request(&discovery_url)?; + + let http_response = http_client.call(discovery_request)?; + + discovery_response(issuer, &discovery_url, http_response) + } + + fn discover_async<'c, C>( + issuer: &IssuerUrl, + http_client: &'c C, + ) -> impl Future> + where + C: AsyncHttpClient<'c>, + C::Error: Send + Sync, + { + Box::pin(async move { + let discovery_url = discovery_url::(issuer)?; + + let discovery_request = discovery_request(&discovery_url)?; + + let http_response = http_client.call(discovery_request).await?; + + discovery_response(issuer, &discovery_url, http_response) + }) + } +} + +fn discovery_url(issuer: &IssuerUrl) -> Result { + issuer + .join(M::METADATA_URL_SUFFIX) + .context("failed to construct metadata URL") +} + +fn discovery_request(discovery_url: &Url) -> Result { + http::Request::builder() + .uri(discovery_url.to_string()) + .method(Method::GET) + .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) + .body(Vec::new()) + .context("failed to prepare request") +} + +fn discovery_response( + issuer: &IssuerUrl, + discovery_url: &Url, + discovery_response: HttpResponse, +) -> Result { + if discovery_response.status() != StatusCode::OK { + bail!( + "HTTP status code {} at {}", + discovery_response.status(), + discovery_url + ) + } + + check_content_type(discovery_response.headers(), MIME_TYPE_JSON)?; + + let metadata = serde_path_to_error::deserialize::<_, M>( + &mut serde_json::Deserializer::from_slice(discovery_response.body()), + )?; + + metadata.validate(issuer)?; + + Ok(metadata) +} diff --git a/src/proof_of_possession.rs b/src/proof_of_possession.rs index f321f5f..4ae2ad7 100644 --- a/src/proof_of_possession.rs +++ b/src/proof_of_possession.rs @@ -1,4 +1,3 @@ -use openidconnect::Nonce; use serde::{Deserialize, Serialize}; use ssi_claims::{ jws::{self, Header}, @@ -9,6 +8,8 @@ use ssi_jwk::{Algorithm, JWKResolver, JWK}; use time::{Duration, OffsetDateTime}; use url::Url; +use crate::types::Nonce; + const JWS_TYPE: &str = "openid4vci-proof+jwt"; pub type ProofSigningAlgValuesSupported = Vec; diff --git a/src/pushed_authorization.rs b/src/pushed_authorization.rs index 8baa83f..a76a3df 100644 --- a/src/pushed_authorization.rs +++ b/src/pushed_authorization.rs @@ -1,11 +1,11 @@ -use std::future::Future; +use std::{borrow::Cow, collections::HashMap, future::Future}; use crate::{ - authorization::AuthorizationDetail, + authorization::{AuthorizationDetail, AuthorizationRequest}, credential::RequestError, http_utils::{content_type_has_essence, MIME_TYPE_FORM_URLENCODED, MIME_TYPE_JSON}, profiles::AuthorizationDetailsProfile, - types::ParUrl, + types::{IssuerState, IssuerUrl, Nonce, ParUrl, UserHint}, }; use oauth2::{ http::{ @@ -14,9 +14,8 @@ use oauth2::{ HeaderValue, Method, StatusCode, }, AsyncHttpClient, AuthUrl, ClientId, CsrfToken, HttpRequest, PkceCodeChallenge, - PkceCodeChallengeMethod, RedirectUrl, + PkceCodeChallengeMethod, RedirectUrl, SyncHttpClient, }; -use openidconnect::{core::CoreErrorResponseType, IssuerUrl, Nonce, StandardErrorResponse}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none}; @@ -40,12 +39,10 @@ impl ParRequestUri { } } -pub type Error = StandardErrorResponse; - #[serde_as] #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct ParAuthParams { +struct ParAuthParams { client_id: ClientId, state: CsrfToken, code_challenge: String, @@ -58,25 +55,8 @@ pub struct ParAuthParams { wallet_issuer: Option, user_hint: Option, issuer_state: Option, -} - -impl ParAuthParams { - field_getters_setters![ - pub self [self] ["ParAuthParams value"] { - set_client_id -> client_id[ClientId], - set_state -> state[CsrfToken], - set_code_challenge -> code_challenge[String], - set_code_challenge_method -> code_challenge_method[PkceCodeChallengeMethod], - set_redirect_uri -> redirect_uri[RedirectUrl], - set_response_type -> response_type[Option], - set_client_assertion -> client_assertion[Option], - set_client_assertion_type -> client_assertion_type[Option], - set_authorization_details -> authorization_details[Option], - set_wallet_issuer -> wallet_issuer[Option], - set_user_hint -> user_hint[Option], - set_issuer_state -> issuer_state[Option], - } - ]; + #[serde(flatten)] + additional_fields: HashMap, } #[derive(Debug, Deserialize, Serialize)] @@ -85,96 +65,78 @@ pub struct PushedAuthorizationResponse { pub expires_in: u64, } -pub struct PushedAuthorizationRequest<'a, AD> -where - AD: AuthorizationDetailsProfile, -{ - inner: oauth2::AuthorizationRequest<'a>, // TODO +pub struct PushedAuthorizationRequest<'a> { + inner: AuthorizationRequest<'a>, par_auth_url: ParUrl, auth_url: AuthUrl, - authorization_details: Vec>, - wallet_issuer: Option, // TODO SIOP related - user_hint: Option, - issuer_state: Option, } -impl<'a, AD> PushedAuthorizationRequest<'a, AD> -where - AD: AuthorizationDetailsProfile, -{ +impl<'a> PushedAuthorizationRequest<'a> { pub(crate) fn new( - inner: oauth2::AuthorizationRequest<'a>, + inner: AuthorizationRequest<'a>, par_auth_url: ParUrl, auth_url: AuthUrl, - authorization_details: Vec>, - wallet_issuer: Option, - user_hint: Option, - issuer_state: Option, ) -> Self { Self { inner, par_auth_url, auth_url, - authorization_details, - wallet_issuer, - user_hint, - issuer_state, } } + pub fn request( + self, + http_client: &C, + ) -> Result<(url::Url, CsrfToken), RequestError<::Error>> + where + C: SyncHttpClient, + { + let mut auth_url = self.auth_url.url().clone(); + + let (http_request, req_body, token) = self + .prepare_request() + .map_err(|err| RequestError::Other(format!("failed to prepare request: {err:?}")))?; + + let http_response = http_client + .call(http_request) + .map_err(RequestError::Request)?; + + let parsed_response = Self::parse_response(http_response)?; + + auth_url + .query_pairs_mut() + .append_pair("request_uri", parsed_response.request_uri.get()); + + auth_url + .query_pairs_mut() + .append_pair("client_id", &req_body.client_id.to_string()); + + Ok((auth_url, token)) + } + pub fn async_request<'c, C>( self, http_client: &'c C, - client_assertion_type: Option, - client_assertion: Option, ) -> impl Future< Output = Result<(url::Url, CsrfToken), RequestError<>::Error>>, > + 'c where 'a: 'c, C: AsyncHttpClient<'c>, - AD: 'c, { Box::pin(async move { let mut auth_url = self.auth_url.url().clone(); - let (http_request, req_body, token) = self - .prepare_request(client_assertion, client_assertion_type) - .map_err(|err| { - RequestError::Other(format!("failed to prepare request: {err:?}")) - })?; + let (http_request, req_body, token) = self.prepare_request().map_err(|err| { + RequestError::Other(format!("failed to prepare request: {err:?}")) + })?; let http_response = http_client .call(http_request) .await .map_err(RequestError::Request)?; - if http_response.status() != StatusCode::OK { - return Err(RequestError::Response( - http_response.status(), - http_response.body().to_owned(), - "unexpected HTTP status code".to_string(), - )); - } - - let parsed_response: PushedAuthorizationResponse = match http_response - .headers() - .get(CONTENT_TYPE) - .map(ToOwned::to_owned) - .unwrap_or_else(|| HeaderValue::from_static(MIME_TYPE_JSON)) - { - ref content_type if content_type_has_essence(content_type, MIME_TYPE_JSON) => { - serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice( - &http_response.body().to_owned(), - )) - .map_err(RequestError::Parse) - } - ref content_type => Err(RequestError::Response( - http_response.status(), - http_response.body().to_owned(), - format!("unexpected response Content-Type: `{:?}`", content_type), - )), - }?; + let parsed_response = Self::parse_response(http_response)?; auth_url .query_pairs_mut() @@ -188,29 +150,13 @@ where }) } - pub fn prepare_request( + fn prepare_request( self, - client_assertion: Option, - client_assertion_type: Option, ) -> Result<(HttpRequest, ParAuthParams, CsrfToken), RequestError> { let (url, token) = self.inner.url(); - let body = serde_urlencoded::from_str::(url.clone().as_str()) - .map_err(|_| RequestError::Other("failed parsing url".to_string()))? - .set_client_assertion_type(client_assertion_type.clone()) - .set_client_assertion(client_assertion.clone()) - .set_authorization_details(Some( - serde_json::to_string::>>(&self.authorization_details) - .map_err(|e| { - RequestError::Other(format!( - "unable to serialize authorization_details: {}", - e - )) - })?, - )) - .set_wallet_issuer(self.wallet_issuer) - .set_user_hint(self.user_hint) - .set_issuer_state(self.issuer_state); + let body = serde_urlencoded::from_str::(url.query().unwrap_or_default()) + .map_err(|_| RequestError::Other("failed parsing url".to_string()))?; let request = http::Request::builder() .uri(self.par_auth_url.to_string()) @@ -232,15 +178,81 @@ where Ok((request, body, token)) } + fn parse_response( + http_response: http::Response>, + ) -> Result> { + if http_response.status() != StatusCode::OK { + return Err(RequestError::Response( + http_response.status(), + http_response.body().to_owned(), + "unexpected HTTP status code".to_string(), + )); + } + + match http_response + .headers() + .get(CONTENT_TYPE) + .map(ToOwned::to_owned) + .unwrap_or_else(|| HeaderValue::from_static(MIME_TYPE_JSON)) + { + ref content_type if content_type_has_essence(content_type, MIME_TYPE_JSON) => { + serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice( + &http_response.body().to_owned(), + )) + .map_err(RequestError::Parse) + } + ref content_type => Err(RequestError::Response( + http_response.status(), + http_response.body().to_owned(), + format!("unexpected response Content-Type: `{:?}`", content_type), + )), + } + } + pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self { self.inner = self.inner.set_pkce_challenge(pkce_code_challenge); self } - pub fn set_authorization_details( + + pub fn set_authorization_details( mut self, authorization_details: Vec>, - ) -> Self { - self.authorization_details = authorization_details; + ) -> Result { + self.inner = self + .inner + .set_authorization_details(authorization_details)?; + Ok(self) + } + + pub fn set_issuer_state(mut self, issuer_state: &'a IssuerState) -> Self { + self.inner = self.inner.set_issuer_state(issuer_state); + self + } + + pub fn set_user_hint(mut self, user_hint: &'a UserHint) -> Self { + self.inner = self.inner.set_user_hint(user_hint); + self + } + + pub fn set_wallet_issuer(mut self, wallet_issuer: &'a IssuerUrl) -> Self { + self.inner = self.inner.set_wallet_issuer(wallet_issuer); + self + } + + pub fn set_client_assertion(self, client_assertion: String) -> Self { + self.add_extra_param("client_assertion", client_assertion) + } + + pub fn set_client_assertion_type(self, client_assertion_type: String) -> Self { + self.add_extra_param("client_assertion_type", client_assertion_type) + } + + pub fn add_extra_param(mut self, name: N, value: V) -> Self + where + N: Into>, + V: Into>, + { + self.inner = self.inner.add_extra_param(name, value); self } } @@ -251,7 +263,11 @@ mod test { use oauth2::{AuthUrl, ClientId, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenUrl}; use serde_json::json; - use crate::{core::profiles::CoreProfilesAuthorizationDetails, metadata::CredentialUrl}; + use crate::{ + core::{metadata::CredentialIssuerMetadata, profiles::CoreProfilesAuthorizationDetails}, + metadata::AuthorizationServerMetadata, + types::CredentialUrl, + }; use super::*; @@ -263,18 +279,33 @@ mod test { "code_challenge": "MYdqq2Vt_ZLMAWpXXsjGIrlxrCF2e4ZP4SxDf7cm_tg", "code_challenge_method": "S256", "redirect_uri": "https://client.example.org/cb", - + "response_type": "code", "authorization_details": "[]", }); - let client = crate::core::client::Client::new( - ClientId::new("s6BhdRkqt3".to_string()), - IssuerUrl::new("https://server.example.com".into()).unwrap(), + let issuer = IssuerUrl::new("https://server.example.com".into()).unwrap(); + + let credential_issuer_metadata = CredentialIssuerMetadata::new( + issuer.clone(), CredentialUrl::new("https://server.example.com/credential".into()).unwrap(), - AuthUrl::new("https://server.example.com/authorize".into()).unwrap(), - Some(ParUrl::new("https://server.example.com/as/par".into()).unwrap()), + ); + + let authorization_server_metadata = AuthorizationServerMetadata::new( + issuer, TokenUrl::new("https://server.example.com/token".into()).unwrap(), + ) + .set_authorization_endpoint(Some( + AuthUrl::new("https://server.example.com/authorize".into()).unwrap(), + )) + .set_pushed_authorization_request_endpoint(Some( + ParUrl::new("https://server.example.com/as/par".into()).unwrap(), + )); + + let client = crate::core::client::Client::from_issuer_metadata( + ClientId::new("s6BhdRkqt3".to_string()), RedirectUrl::new("https://client.example.org/cb".into()).unwrap(), + credential_issuer_metadata, + authorization_server_metadata, ); let pkce_verifier = @@ -283,10 +314,12 @@ mod test { let state = CsrfToken::new("state".into()); let (_, body, _) = client - .pushed_authorization_request::<_, CoreProfilesAuthorizationDetails>(move || state) + .pushed_authorization_request(move || state) .unwrap() .set_pkce_challenge(pkce_challenge) - .prepare_request(None, None) + .set_authorization_details::(vec![]) + .unwrap() + .prepare_request() .unwrap(); assert_json_eq!(expected_body, body); } diff --git a/src/token.rs b/src/token.rs index d84af6f..09382a9 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,15 +1,15 @@ use std::time::Duration; -use oauth2::AuthorizationCode; -use openidconnect::{ - core::{CoreErrorResponseType, CoreTokenType}, - ClientId, Nonce, RedirectUrl, StandardErrorResponse, StandardTokenResponse, +use oauth2::basic::BasicTokenType; +use oauth2::{ + AuthorizationCode, ClientId, ExtraTokenFields, RedirectUrl, RefreshToken, StandardTokenResponse, }; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none}; -use crate::profiles::AuthorizationDetailsProfile; +use crate::types::{Nonce, PreAuthorizedCode}; use crate::{authorization::AuthorizationDetail, core::profiles::CoreProfilesAuthorizationDetails}; +use crate::{profiles::AuthorizationDetailsProfile, types::TxCode}; #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case", tag = "grant_type")] @@ -23,16 +23,13 @@ pub enum Request { PreAuthorizedCode { client_id: Option, #[serde(rename = "pre-authorized_code")] - pre_authorized_code: String, - #[serde(alias = "pin")] - user_pin: Option, + pre_authorized_code: PreAuthorizedCode, + tx_code: Option, }, - #[serde(rename = "urn:ietf:params:oauth:grant-type:refresh_token")] + #[serde(rename = "refresh_token")] RefreshToken { client_id: Option, - refresh_token: String, - #[serde(alias = "pin")] - user_pin: Option, + refresh_token: RefreshToken, }, } @@ -51,30 +48,7 @@ where pub type Response = StandardTokenResponse< ExtraResponseTokenFields, - CoreTokenType, + BasicTokenType, >; -/// The following additional error codes, defined in RFC8628, are -/// mentioned and can be used as follow: -/// ``` -/// use openidconnect::core::CoreErrorResponseType; -/// use oid4vci::token::Error; -/// -/// let auth_pending_err = Error::new( -/// CoreErrorResponseType::Extension("authorization_pending".to_string()), -/// None, -/// None, -/// ); -/// -/// let slow_down_err = Error::new( -/// CoreErrorResponseType::Extension("slow_down".to_string()), -/// None, -/// None, -/// ); -/// ``` -pub type Error = StandardErrorResponse; - -impl openidconnect::ExtraTokenFields for ExtraResponseTokenFields where - AD: AuthorizationDetailsProfile -{ -} +impl ExtraTokenFields for ExtraResponseTokenFields where AD: AuthorizationDetailsProfile {} diff --git a/src/types.rs b/src/types.rs index ce44609..bf03816 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,9 @@ +use std::fmt::{Debug, Error as FormatterError, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::Deref; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use url::Url; macro_rules! new_type { @@ -13,18 +16,11 @@ macro_rules! new_type { ) ) => { new_type![ - @new_type $(#[$attr])*, + $(#[$attr])* $name( $(#[$type_attr])* $type - ), - concat!( - "Create a new `", - stringify!($name), - "` to wrap the given `", - stringify!($type), - "`." - ), + ) impl {} ]; }; @@ -96,6 +92,89 @@ macro_rules! new_type { } } } + +macro_rules! new_secret_type { + ( + $(#[$attr:meta])* + $name:ident($type:ty) + ) => { + new_secret_type![ + $(#[$attr])* + $name($type) + impl {} + ]; + }; + ( + $(#[$attr:meta])* + $name:ident($type:ty) + impl { + $($item:tt)* + } + ) => { + new_secret_type![ + $(#[$attr])*, + $name($type), + concat!( + "Create a new `", + stringify!($name), + "` to wrap the given `", + stringify!($type), + "`." + ), + concat!("Get the secret contained within this `", stringify!($name), "`."), + impl { + $($item)* + } + ]; + }; + ( + $(#[$attr:meta])*, + $name:ident($type:ty), + $new_doc:expr, + $secret_doc:expr, + impl { + $($item:tt)* + } + ) => { + $( + #[$attr] + )* + pub struct $name($type); + impl $name { + $($item)* + + #[doc = $new_doc] + pub fn new(s: $type) -> Self { + $name(s) + } + #[doc = $secret_doc] + /// + /// # Security Warning + /// + /// Leaking this value may compromise the security of the OAuth2 flow. + pub fn secret(&self) -> &$type { &self.0 } + } + impl Debug for $name { + fn fmt(&self, f: &mut Formatter) -> Result<(), FormatterError> { + write!(f, concat!(stringify!($name), "([redacted])")) + } + } + + impl PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + Sha256::digest(&self.0) == Sha256::digest(&other.0) + } + } + + impl Hash for $name { + fn hash(&self, state: &mut H) { + Sha256::digest(&self.0).hash(state) + } + } + + }; +} + /// /// Creates a URL-specific new type /// @@ -115,11 +194,8 @@ macro_rules! new_url_type { $name:ident ) => { new_url_type![ - @new_type_pub $(#[$attr])*, - $name, - concat!("Create a new `", stringify!($name), "` from a `String` to wrap a URL."), - concat!("Create a new `", stringify!($name), "` from a `Url` to wrap a URL."), - concat!("Return this `", stringify!($name), "` as a parsed `Url`."), + $(#[$attr])* + $name impl {} ]; }; @@ -244,62 +320,92 @@ macro_rules! new_url_type { } new_url_type![ - /// + /// Base URL of the [Credential] Issuer. + IssuerUrl + impl { + /// Parse a string as a URL, with this URL as the base URL. + /// + /// See [`Url::parse`]. + pub fn join(&self, suffix: &str) -> Result { + if let Some('/') = self.1.chars().next_back() { + Url::parse(&(self.1.clone() + suffix)) + } else { + Url::parse(&(self.1.clone() + "/" + suffix)) + } + } + } +]; + +new_url_type![ /// URL of the Credential Issuer's Credential Endpoint. - /// CredentialUrl ]; new_url_type![ - /// /// URL of the Credential Issuer's Batch Credential Endpoint. - /// BatchCredentialUrl ]; new_url_type![ - /// /// URL of the Credential Issuer's Deferred Credential Endpoint. - /// DeferredCredentialUrl ]; new_url_type![ - /// /// URL of the Pushed Authorization Request Endpoint. - /// ParUrl ]; new_url_type![ - /// /// URL of the Credential Issuer's Notification Endpoint - /// NotificationUrl ]; new_url_type![ - /// - /// URI where the Wallet can obtain an image - /// - ImageUrl + /// URL of the authorization server's JWK Set document + /// (see [RFC7517](https://datatracker.ietf.org/doc/html/rfc7517)). + JsonWebKeySetUrl +]; + +new_url_type![ + /// URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint + /// (see [RFC7591](https://datatracker.ietf.org/doc/html/rfc7591)). + RegistrationUrl +]; + +new_url_type![ + /// A URI where the Wallet can obtain the logo of the Credential from the Credential Issuer. + /// The Wallet needs to determine the scheme, since the URI value could use the `https:` scheme, + /// the `data:` scheme, etc. + LogoUri +]; + +new_type![ + /// String value that identifies the language of this object represented as a language tag taken + /// from values defined in [BCP47 (RFC5646)](https://www.rfc-editor.org/rfc/rfc5646.html). + #[derive(Deserialize, Serialize, Eq, Hash)] + LanguageTag(String) ]; new_type![ - /// - /// String value of a background color of the Credential represented as numerical color values defined in CSS Color Module Level 37 [CSS-Color]. - /// + /// String value of a background color of the Credential represented as numerical color values + /// defined in [CSS Color Module Level 37](https://www.w3.org/TR/css-color-3). #[derive(Deserialize, Serialize, Eq, Hash)] BackgroundColor(String) ]; + new_type![ - /// - /// String value of a text color of the Credential represented as numerical color values defined in CSS Color Module Level 37 [CSS-Color]. - /// + /// String value of a text color of the Credential represented as numerical color values + /// defined in [CSS Color Module Level 37](https://www.w3.org/TR/css-color-3). #[derive(Deserialize, Serialize, Eq, Hash)] TextColor(String) ]; +new_type![ + #[derive(Deserialize, Serialize, Eq, Hash)] + ResponseMode(String) +]; + new_type![ #[derive(Deserialize, Eq, Hash, Ord, PartialOrd, Serialize)] JsonWebTokenContentType(String) @@ -309,3 +415,34 @@ new_type![ #[derive(Deserialize, Eq, Hash, Ord, PartialOrd, Serialize)] JsonWebTokenType(String) ]; + +new_secret_type![ + #[derive(Deserialize, Serialize, Clone)] + Nonce(String) + impl { + pub fn new_random() -> Self { + use base64::prelude::*; + Self(BASE64_URL_SAFE_NO_PAD.encode(rand::random::<[u8; 16]>())) + } + } +]; + +new_secret_type![ + #[derive(Deserialize, Serialize, Clone)] + PreAuthorizedCode(String) +]; + +new_secret_type![ + #[derive(Deserialize, Serialize, Clone)] + IssuerState(String) +]; + +new_secret_type![ + #[derive(Deserialize, Serialize)] + UserHint(String) +]; + +new_secret_type![ + #[derive(Deserialize, Serialize)] + TxCode(String) +];