Skip to content

Commit

Permalink
Various updates
Browse files Browse the repository at this point in the history
- rename crate
- make http client to be configurable
- refactor traits and request builder
- add client metadata resolution
  • Loading branch information
cobward committed Oct 23, 2023
1 parent da43e65 commit cd0f80d
Show file tree
Hide file tree
Showing 15 changed files with 335 additions and 539 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on: [push, pull_request]

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-Dwarnings"

jobs:
build:
Expand All @@ -18,5 +19,5 @@ jobs:
run: cargo test
- name: Fmt
run: cargo fmt -- --check
# - name: Clippy
# run: RUSTFLAGS="-Dwarnings" cargo clippy
- name: Clippy
run: cargo clippy
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ anyhow = "1.0.75"
async-trait = "0.1.73"
base64 = "0.21.4"
did-web = "0.2.2"
isomdl = { git = "https://[email protected]/spruceid/isomdl", rev = "b2324b7" }
josekit = { git = "https://github.com/cobward/josekit-rs", rev = "635c8a7" }
p256 = { version = "0.13.2", features = ["jwk"] }
reqwest = "0.11.20"
Expand Down
103 changes: 76 additions & 27 deletions src/core/authorization_request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ use url::Url;

use self::{
parameters::{
ClientId, ClientIdScheme, PresentationDefinition, PresentationDefinitionUri, RedirectUri,
ResponseMode, ResponseType, ResponseUri,
ClientId, ClientIdScheme, Nonce, PresentationDefinition, PresentationDefinitionUri,
RedirectUri, ResponseMode, ResponseType, ResponseUri,
},
verification::verify_request,
};

use super::{
object::{ParsingErrorContext, UntypedObject},
profile::WalletProfile,
util::http_client,
profile::Wallet,
util::default_http_client,
};

pub mod parameters;
Expand All @@ -32,6 +32,7 @@ pub struct AuthorizationRequestObject(
ResponseType,
PresentationDefinitionIndirection,
Url,
Nonce,
);

/// An Authorization Request.
Expand Down Expand Up @@ -63,23 +64,25 @@ impl AuthorizationRequest {
/// [RequestObject].
///
/// Custom wallet metadata can be provided, otherwise the default metadata for this profile is used.
pub async fn validate<WP: WalletProfile + ?Sized>(
pub async fn validate_with_http_client<WP: Wallet + ?Sized>(
self,
wallet_profile: &WP,
http_client: &reqwest::Client,
) -> Result<AuthorizationRequestObject> {
let jwt = match self.request_indirection {
RequestIndirection::ByValue(jwt) => jwt,
RequestIndirection::ByReference(url) => http_client()?
RequestIndirection::ByReference(url) => http_client
.get(url.clone())
.header("Prefer", "OID4VP-0.0.20")
.send()
.await
.context(format!("failed to GET {url}"))?
.error_for_status()
.context(format!("failed to GET {url}"))?
.text()
.await
.context(format!("failed to parse data from {url}"))?,
};
let aro = verify_request(wallet_profile, jwt)
let aro = verify_request(wallet_profile, jwt, http_client)
.await
.context("unable to validate Authorization Request")?;
if self.client_id.as_str() != aro.client_id().0.as_str() {
Expand All @@ -92,15 +95,28 @@ impl AuthorizationRequest {
Ok(aro)
}

/// Validate the [AuthorizationRequest] according to the client_id scheme and return the parsed
/// [RequestObject].
///
/// Custom wallet metadata can be provided, otherwise the default metadata for this profile is used.
///
/// This method uses the library default http client to fetch the request object if it is passed by reference.
pub async fn validate<WP: Wallet + ?Sized>(
self,
wallet_profile: &WP,
) -> Result<AuthorizationRequestObject> {
self.validate_with_http_client(wallet_profile, &default_http_client()?)
.await
}

/// Encode as [Url], using the `authorization_endpoint` as a base.
/// ```
/// # use verifier_api::mock::authorization_request::AuthorizationRequest;
/// # use verifier_api::mock::authorization_request::RequestIndirection;
/// # use verifier_api::mock::client_id::ClientId;
/// # use oid4vp::core::authorization_request::AuthorizationRequest;
/// # use oid4vp::core::authorization_request::RequestIndirection;
/// # use url::Url;
/// let authorization_endpoint: Url = "example://".parse().unwrap();
/// let authorization_request = AuthorizationRequest {
/// client_id: ClientId("xyz".to_string()),
/// client_id: "xyz".to_string(),
/// request_indirection: RequestIndirection::ByValue("test".to_string()),
/// };
///
Expand All @@ -116,41 +132,54 @@ impl AuthorizationRequest {

/// Parse from [Url], validating the authorization_endpoint.
/// ```
/// # use verifier_api::mock::authorization_request::AuthorizationRequest;
/// # use verifier_api::mock::authorization_request::RequestIndirection;
/// # use oid4vp::core::authorization_request::AuthorizationRequest;
/// # use oid4vp::core::authorization_request::RequestIndirection;
/// # use url::Url;
/// let url: Url = "example://?client_id=xyz&request=test".parse().unwrap();
/// let authorization_endpoint: Url = "example://".parse().unwrap();
///
/// let authorization_request = AuthorizationRequest::from_url(url, &authorization_endpoint).unwrap();
/// let authorization_request = AuthorizationRequest::from_url(
/// url,
/// &authorization_endpoint
/// ).unwrap();
///
/// assert_eq!(authorization_request.client_id.0, "xyz");
/// assert_eq!(authorization_request.client_id, "xyz");
///
/// let RequestIndirection::ByValue(request_object) =
/// authorization_request.request_indirection
/// else {
/// panic!("expected request-by-value")
/// };
///
/// let RequestIndirection::ByValue(request_object) = authorization_request.request_indirection
/// else { panic!("expected request-by-value") };
/// assert_eq!(request_object, "test");
/// ```
pub fn from_url(mut url: Url, authorization_endpoint: &Url) -> Result<Self> {
pub fn from_url(url: Url, authorization_endpoint: &Url) -> Result<Self> {
let query = url
.query()
.ok_or(anyhow!("missing query params in Authorization Request uri"))?
.to_string();
url.set_query(None);
if &url != authorization_endpoint {
bail!("unexpected authorization_endpoint, expected '{authorization_endpoint}', received '{url}'")
let fnd = url.authority();
let exp = authorization_endpoint.authority();
if fnd != exp {
bail!("unexpected authorization_endpoint authority, expected '{exp}', received '{fnd}'")
}
let fnd = url.path();
let exp = authorization_endpoint.path();
if fnd != exp {
bail!("unexpected authorization_endpoint path, expected '{exp}', received '{fnd}'")
}
Self::from_query_params(&query)
}

/// Parse from urlencoded query parameters.
/// ```
/// # use verifier_api::mock::authorization_request::AuthorizationRequest;
/// # use verifier_api::mock::authorization_request::RequestIndirection;
/// # use oid4vp::core::authorization_request::AuthorizationRequest;
/// # use oid4vp::core::authorization_request::RequestIndirection;
/// let query = "client_id=xyz&request=test";
///
/// let authorization_request = AuthorizationRequest::from_query_params(query).unwrap();
///
/// assert_eq!(authorization_request.client_id.0, "xyz");
/// assert_eq!(authorization_request.client_id, "xyz");
///
/// let RequestIndirection::ByValue(request_object) = authorization_request.request_indirection
/// else { panic!("expected request-by-value") };
Expand All @@ -171,17 +200,24 @@ impl AuthorizationRequestObject {
&self.2
}

pub async fn presentation_definition(&self) -> Result<PresentationDefinition> {
pub async fn resolve_presentation_definition_with_http_client(
&self,
http_client: reqwest::Client,
) -> Result<PresentationDefinition> {
match &self.5 {
PresentationDefinitionIndirection::ByValue(by_value) => Ok(by_value.clone()),
PresentationDefinitionIndirection::ByReference(by_reference) => {
let value: Json = http_client()?
let value: Json = http_client
.get(by_reference.clone())
.send()
.await
.context(format!(
"failed to GET Presentation Definition from '{by_reference}'"
))?
.error_for_status()
.context(format!(
"failed to GET Presentation Definition from '{by_reference}'"
))?
.json()
.await
.context(format!(
Expand All @@ -192,6 +228,12 @@ impl AuthorizationRequestObject {
}
}

/// Uses the default library http client.
pub async fn resolve_presentation_definition(&self) -> Result<PresentationDefinition> {
self.resolve_presentation_definition_with_http_client(default_http_client()?)
.await
}

pub fn is_id_token_requested(&self) -> Option<bool> {
match self.4 {
ResponseType::VpToken => Some(false),
Expand All @@ -214,6 +256,10 @@ impl AuthorizationRequestObject {
pub fn return_uri(&self) -> &Url {
&self.6
}

pub fn nonce(&self) -> &Nonce {
&self.7
}
}

impl From<AuthorizationRequestObject> for UntypedObject {
Expand Down Expand Up @@ -279,6 +325,8 @@ impl TryFrom<UntypedObject> for AuthorizationRequestObject {
}
};

let nonce = value.get().parsing_error()?;

Ok(Self(
value,
client_id,
Expand All @@ -287,6 +335,7 @@ impl TryFrom<UntypedObject> for AuthorizationRequestObject {
response_type,
pd_indirection,
return_uri,
nonce,
))
}
}
Expand Down
96 changes: 91 additions & 5 deletions src/core/authorization_request/parameters.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use std::fmt;

use crate::core::object::{TypedParameter, UntypedObject};
use anyhow::Error;
use crate::core::{
object::{ParsingErrorContext, TypedParameter, UntypedObject},
util::default_http_client,
};
use anyhow::{bail, Context, Error, Ok};
use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use url::Url;

use super::AuthorizationRequestObject;

const DID: &str = "did";
const ENTITY_ID: &str = "entity_id";
const PREREGISTERED: &str = "pre-registered";
Expand Down Expand Up @@ -130,6 +135,51 @@ impl TryFrom<Json> for ClientMetadata {
}
}

impl ClientMetadata {
/// Resolves the client metadata from the Authorization Request Object.
///
/// If the client metadata is not passed by reference or value if the Authorization Request Object,
/// then this function will return an error.
///
/// Uses the library's default http client.
pub async fn resolve(request: &AuthorizationRequestObject) -> Result<Self, Error> {
Self::resolve_with_http_client(request, &default_http_client()?).await
}

/// Resolves the client metadata from the Authorization Request Object.
///
/// If the client metadata is not passed by reference or value if the Authorization Request Object,
/// then this function will return an error.
pub async fn resolve_with_http_client(
request: &AuthorizationRequestObject,
http_client: &reqwest::Client,
) -> Result<Self, Error> {
if let Some(metadata) = request.get() {
return metadata;
}

if let Some(metadata_uri) = request.get::<ClientMetadataUri>() {
let uri = metadata_uri.parsing_error()?;
return http_client
.get(uri.0.clone())
.send()
.await
.context(format!("failed to GET {}", uri.0))?
.error_for_status()
.context(format!("failed to GET {}", uri.0))?
.json()
.await
.map(ClientMetadata)
.context(format!(
"could not parse response from GET '{}' as JSON",
uri.0
));
}

bail!("")
}
}

/// `client_metadata_uri` field in the Authorization Request.
#[derive(Debug, Clone)]
pub struct ClientMetadataUri(pub Url);
Expand Down Expand Up @@ -308,6 +358,16 @@ impl Default for ResponseMode {
}
}

impl ResponseMode {
pub fn is_jarm(&self) -> Result<bool, Error> {
match self {
ResponseMode::DirectPost => Ok(false),
ResponseMode::DirectPostJwt => Ok(true),
ResponseMode::Unsupported(rm) => bail!("unsupported response_mode: {rm}"),
}
}
}

const VP_TOKEN: &str = "vp_token";
const VP_TOKEN_ID_TOKEN: &str = "vp_token id_token";

Expand Down Expand Up @@ -379,8 +439,33 @@ impl From<State> for Json {
}
}

// TODO: Revisit the inner parsed type.
#[derive(Debug, Clone)]
pub struct PresentationDefinition(pub Json);
pub struct PresentationDefinition {
raw: Json,
parsed: crate::presentation_exchange::PresentationDefinition,
}

impl PresentationDefinition {
pub fn into_parsed(self) -> crate::presentation_exchange::PresentationDefinition {
self.parsed
}

pub fn parsed(&self) -> &crate::presentation_exchange::PresentationDefinition {
&self.parsed
}
}

impl TryFrom<crate::presentation_exchange::PresentationDefinition> for PresentationDefinition {
type Error = Error;

fn try_from(
parsed: crate::presentation_exchange::PresentationDefinition,
) -> Result<Self, Self::Error> {
let raw = serde_json::to_value(parsed.clone())?;
Ok(Self { raw, parsed })
}
}

impl TypedParameter for PresentationDefinition {
const KEY: &'static str = "presentation_definition";
Expand All @@ -390,13 +475,14 @@ impl TryFrom<Json> for PresentationDefinition {
type Error = Error;

fn try_from(value: Json) -> Result<Self, Self::Error> {
Ok(value).map(Self)
let parsed = serde_json::from_value(value.clone())?;
Ok(Self { raw: value, parsed })
}
}

impl From<PresentationDefinition> for Json {
fn from(value: PresentationDefinition) -> Self {
value.0
value.raw
}
}

Expand Down
Loading

0 comments on commit cd0f80d

Please sign in to comment.