Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor in core. #3

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[net]
git-fetch-with-cli = true
29 changes: 19 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,28 @@ repository = "https://github.com/spruceid/oidc4vp-rs/"
documentation = "https://docs.rs/oidc4vp/"

[dependencies]
# openidconnect = { version = "2.4.0", default-features = false }
jsonpath-rust = "0.2.0"
serde = { version = "1.0.147", features = ["derive"] }
serde_json = "1.0.87"
ssi = { version = "0.6.0", default-features = false }
thiserror = "1.0.37"
# jsonschema = { version = "0.16.1", default-features = false }
lazy_static = "1.4.0"
# schemars = { version = "0.8.11", default-features = false }
anyhow = "1.0.75"
async-trait = "0.1.73"
base64 = "0.21.4"
did-web = "0.2.2"
didkit = "0.6.0"
sbihel marked this conversation as resolved.
Show resolved Hide resolved
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"
serde = "1.0.188"
serde_cbor = "0.11.2"
serde_json = "1.0.107"
serde_qs = "0.12.0"
serde_urlencoded = "0.7.1"
thiserror = "1.0.49"
tracing = "0.1.37"
url = { version = "2.4.1", features = ["serde"] }
x509-cert = "0.2.4"

[dev-dependencies]
serde_path_to_error = "0.1.8"

tokio = { version = "1.32.0", features = ["macros"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
uuid = { version = "1.2", features = ["v4", "serde", "js"] }
Expand Down
306 changes: 306 additions & 0 deletions src/core/authorization_request/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
use std::ops::{Deref, DerefMut};

use anyhow::{anyhow, bail, Context, Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use url::Url;

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

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

pub mod parameters;
pub mod verification;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(try_from = "UntypedObject", into = "UntypedObject")]
pub struct AuthorizationRequestObject(
UntypedObject,
ClientId,
ClientIdScheme,
ResponseMode,
ResponseType,
PresentationDefinitionIndirection,
Url,
);

/// An Authorization Request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthorizationRequest {
pub client_id: String,
#[serde(flatten)]
pub request_indirection: RequestIndirection,
}

/// A RequestObject, passed by value or by reference.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RequestIndirection {
#[serde(rename = "request")]
ByValue(String),
#[serde(rename = "request_uri")]
ByReference(Url),
}

/// A PresentationDefinition, passed by value or by reference
#[derive(Debug, Clone)]
pub enum PresentationDefinitionIndirection {
ByValue(PresentationDefinition),
ByReference(Url),
}

impl AuthorizationRequest {
/// 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.
pub async fn validate<WP: WalletProfile + ?Sized>(
self,
wallet_profile: &WP,
) -> Result<AuthorizationRequestObject> {
let jwt = match self.request_indirection {
RequestIndirection::ByValue(jwt) => jwt,
RequestIndirection::ByReference(url) => http_client()?
.get(url.clone())
.header("Prefer", "OID4VP-0.0.20")
.send()
.await
.context(format!("failed to GET {url}"))?
.text()
.await
.context(format!("failed to parse data from {url}"))?,
};
let aro = verify_request(wallet_profile, jwt)
.await
.context("unable to validate Authorization Request")?;
if self.client_id.as_str() != aro.client_id().0.as_str() {
bail!(
"Authorization Request and Request Object have different client ids: '{}' vs. '{}'",
self.client_id,
aro.client_id().0
);
}
Ok(aro)
}

/// 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 url::Url;
/// let authorization_endpoint: Url = "example://".parse().unwrap();
/// let authorization_request = AuthorizationRequest {
/// client_id: ClientId("xyz".to_string()),
/// request_indirection: RequestIndirection::ByValue("test".to_string()),
/// };
///
/// let authorization_request_url = authorization_request.to_url(authorization_endpoint).unwrap();
///
/// assert_eq!(authorization_request_url.as_str(), "example://?client_id=xyz&request=test");
/// ```
pub fn to_url(self, mut authorization_endpoint: Url) -> Result<Url> {
let query = serde_urlencoded::to_string(self)?;
authorization_endpoint.set_query(Some(&query));
Ok(authorization_endpoint)
}

/// Parse from [Url], validating the authorization_endpoint.
/// ```
/// # use verifier_api::mock::authorization_request::AuthorizationRequest;
/// # use verifier_api::mock::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();
///
/// assert_eq!(authorization_request.client_id.0, "xyz");
///
/// 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> {
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}'")
}
Self::from_query_params(&query)
}

/// Parse from urlencoded query parameters.
/// ```
/// # use verifier_api::mock::authorization_request::AuthorizationRequest;
/// # use verifier_api::mock::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");
///
/// let RequestIndirection::ByValue(request_object) = authorization_request.request_indirection
/// else { panic!("expected request-by-value") };
/// assert_eq!(request_object, "test");
/// ```
pub fn from_query_params(query_params: &str) -> Result<Self> {
serde_urlencoded::from_str(query_params)
.context("unable to parse Authorization Request from query params")
}
}

impl AuthorizationRequestObject {
pub fn client_id(&self) -> &ClientId {
&self.1
}

pub fn client_id_scheme(&self) -> &ClientIdScheme {
&self.2
}

pub async fn presentation_definition(&self) -> Result<PresentationDefinition> {
match &self.5 {
PresentationDefinitionIndirection::ByValue(by_value) => Ok(by_value.clone()),
PresentationDefinitionIndirection::ByReference(by_reference) => {
let value: Json = http_client()?
.get(by_reference.clone())
.send()
.await
.context(format!(
"failed to GET Presentation Definition from '{by_reference}'"
))?
.json()
.await
.context(format!(
"response received from '{by_reference}' was not JSON"
))?;
value.try_into()
}
}
}

pub fn is_id_token_requested(&self) -> Option<bool> {
match self.4 {
ResponseType::VpToken => Some(false),
ResponseType::VpTokenIdToken => Some(true),
ResponseType::Unsupported(_) => None,
}
}

pub fn response_mode(&self) -> &ResponseMode {
&self.3
}

pub fn response_type(&self) -> &ResponseType {
&self.4
}

/// Uri to submit the response at.
///
/// AKA [ResponseUri] or [RedirectUri] depending on [ResponseMode].
pub fn return_uri(&self) -> &Url {
&self.6
}
}

impl From<AuthorizationRequestObject> for UntypedObject {
fn from(value: AuthorizationRequestObject) -> Self {
let mut inner = value.0;
inner.insert(value.1);
inner.insert(value.2);
inner
}
}

impl TryFrom<UntypedObject> for AuthorizationRequestObject {
type Error = Error;

fn try_from(value: UntypedObject) -> std::result::Result<Self, Self::Error> {
let client_id = value.get().parsing_error()?;
let client_id_scheme = value
.get()
.parsing_error()
.context("this library cannot handle requests that omit client_id_scheme")?;

let redirect_uri = value.get::<RedirectUri>();
let response_uri = value.get::<ResponseUri>();

let (return_uri, response_mode) = match (
redirect_uri,
response_uri,
value.get_or_default::<ResponseMode>().parsing_error()?,
) {
(_, _, ResponseMode::Unsupported(m)) => {
bail!("this 'response_mode' ({m}) is not currently supported")
}
(Some(_), Some(_), _) => {
bail!("'response_uri' and 'redirect_uri' are mutually exclusive")
}
(_, None, response_mode @ ResponseMode::DirectPost)
| (_, None, response_mode @ ResponseMode::DirectPostJwt) => {
bail!("'response_uri' is required for this 'response_mode' ({response_mode})")
}
(_, Some(uri), response_mode @ ResponseMode::DirectPost)
| (_, Some(uri), response_mode @ ResponseMode::DirectPostJwt) => {
(uri.parsing_error()?.0, response_mode)
}
};

let response_type: ResponseType = value.get().parsing_error()?;

let pd_indirection = match (
value.get::<PresentationDefinition>(),
value.get::<PresentationDefinitionUri>(),
) {
(None, None) => bail!(
"one of 'presentation_definition' and 'presentation_definition_uri' are required"
),
(Some(_), Some(_)) => {
bail!("'presentation_definition' and 'presentation_definition_uri' are mutually exclusive")
}
(Some(by_value), None) => {
PresentationDefinitionIndirection::ByValue(by_value.parsing_error()?)
}
(None, Some(by_reference)) => {
PresentationDefinitionIndirection::ByReference(by_reference.parsing_error()?.0)
}
};

Ok(Self(
value,
client_id,
client_id_scheme,
response_mode,
response_type,
pd_indirection,
return_uri,
))
}
}

impl Deref for AuthorizationRequestObject {
type Target = UntypedObject;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl DerefMut for AuthorizationRequestObject {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
Loading