Skip to content

Commit

Permalink
feat: improved builder pattern for RequestUrl (#27)
Browse files Browse the repository at this point in the history
* Add support for Request by reference

Add tests for RequestUrl

Add missing request parameters

Add sphereon demo website test

Update documentation with new RequestUrl

Remove sphereon demo example

Add validate_request method to Provider struct

Add preoper Ser and De for SiopRequest and RequestBuilder

Add skeptic for Markdown code testing

Add support for Request by reference

fix: fix rebase conflicts

Add comments and fix some tests

fix: Move `derivative` to dev-dependencies

Refactor Provider and Subject

improve tests and example using wiremock

Improve struct field serde

fix: remove claims from lib.rs

style: fix arguments order

Add did:key DID method

Add support for Request by reference

fix: Remove lifetime annotations

Add preoper Ser and De for SiopRequest and RequestBuilder

Add Scope and Claim

fix: fix rebase conflicts

* fix: remove custom serde

* Add claims and scope parameters

* Add Storage and RelyingParty test improvement

* Update README example

* fix: Add standard_claims to test IdToken

* Move Storage trait to test_utils

* Remove storage.rs

* fix: fex rebase to dev

* fix: fix rebase to dev

* feat: add Claim trait with associated types

* fix: build

* fix: remove build.rs and change crate name in doc tests

* feat: refactor claims.rs

* fix: remove skeptic crate

* feat: allow json arguments for claims() method

* style: add specific request folder

* test: improve RequestBuilder tests

* fix: fix rebase

* Add support for Request by reference

Add tests for RequestUrl

Add missing request parameters

Add sphereon demo website test

Update documentation with new RequestUrl

Remove sphereon demo example

Add validate_request method to Provider struct

Add preoper Ser and De for SiopRequest and RequestBuilder

Add skeptic for Markdown code testing

Add support for Request by reference

fix: fix rebase conflicts

Add comments and fix some tests

fix: Move `derivative` to dev-dependencies

Refactor Provider and Subject

improve tests and example using wiremock

Improve struct field serde

fix: remove claims from lib.rs

style: fix arguments order

Add did:key DID method

Add support for Request by reference

fix: Remove lifetime annotations

Add preoper Ser and De for SiopRequest and RequestBuilder

Add Scope and Claim

fix: fix rebase conflicts

* fix: remove custom serde

* Add claims and scope parameters

* Add Storage and RelyingParty test improvement

* Update README example

* fix: Add standard_claims to test IdToken

* Move Storage trait to test_utils

* fix: fex rebase to dev

* feat: add Claim trait with associated types

* feat: refactor claims.rs

* feat: Add builder for Response and IdToken

* feat: add missing ID Token claim parameters

* style: rename request and response

* feat: improve request builder

* fix: fix rebase

* fix: fix doctest
  • Loading branch information
nanderstabel authored Jun 9, 2023
1 parent 636a1e3 commit a81b8c2
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 88 deletions.
28 changes: 18 additions & 10 deletions siopv2/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,30 @@ where
/// request by value. If the [`RequestUrl`] is a request by value, the request is decoded by the [`Subject`] of the [`Provider`].
/// If the request is valid, the request is returned.
pub async fn validate_request(&self, request: RequestUrl) -> Result<AuthorizationRequest> {
let request = match request {
RequestUrl::AuthorizationRequest(request) => *request,
RequestUrl::RequestUri { request_uri } => {
let client = reqwest::Client::new();
let builder = client.get(request_uri);
let request_value = builder.send().await?.text().await?;
self.subject.decode(request_value).await?
}
let authorization_request = if let RequestUrl::Request(request) = request {
*request
} else {
let (request_object, client_id) = match request {
RequestUrl::RequestUri { request_uri, client_id } => {
let client = reqwest::Client::new();
let builder = client.get(request_uri);
let request_value = builder.send().await?.text().await?;
(request_value, client_id)
}
RequestUrl::RequestObject { request, client_id } => (request, client_id),
_ => unreachable!(),
};
let authorization_request: AuthorizationRequest = self.subject.decode(request_object).await?;
anyhow::ensure!(*authorization_request.client_id() == client_id, "Client id mismatch.");
authorization_request
};
self.subject_syntax_types_supported().and_then(|supported| {
request.subject_syntax_types_supported().map_or_else(
authorization_request.subject_syntax_types_supported().map_or_else(
|| Err(anyhow!("No supported subject syntax types found.")),
|supported_types| {
supported_types.iter().find(|sst| supported.contains(sst)).map_or_else(
|| Err(anyhow!("Subject syntax type not supported.")),
|_| Ok(request.clone()),
|_| Ok(authorization_request.clone()),
)
},
)
Expand Down
1 change: 1 addition & 0 deletions siopv2/src/relying_party.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ mod tests {

// Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint.
let request_url = RequestUrl::builder()
.client_id("did:mock:1".to_string())
.request_uri(format!("{server_url}/request_uri"))
.build()
.unwrap();
Expand Down
59 changes: 37 additions & 22 deletions siopv2/src/request/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::token::id_token::RFC7519Claims;
use crate::{claims::ClaimRequests, Registration, RequestUrlBuilder, Scope, StandardClaimsRequests};
use anyhow::{anyhow, Result};
use derive_more::Display;
Expand All @@ -20,10 +21,11 @@ pub mod request_builder;
///
/// // An example of a form-urlencoded request with only the `request_uri` parameter will be parsed as a
/// // `RequestUrl::RequestUri` variant.
/// let request_url = RequestUrl::from_str("siopv2://idtoken?request_uri=https://example.com/request_uri").unwrap();
/// let request_url = RequestUrl::from_str("siopv2://idtoken?client_id=did%3Aexample%3AEiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA&request_uri=https://example.com/request_uri").unwrap();
/// assert_eq!(
/// request_url,
/// RequestUrl::RequestUri {
/// client_id: "did:example:EiDrihTRe0GMdc3K16kgJB3Xbl9Hb8oqVHjzm6ufHcYDGA".to_string(),
/// request_uri: "https://example.com/request_uri".to_string()
/// }
/// );
Expand All @@ -45,16 +47,17 @@ pub mod request_builder;
/// )
/// .unwrap();
/// assert!(match request_url {
/// RequestUrl::AuthorizationRequest(_) => Ok(()),
/// RequestUrl::Request(_) => Ok(()),
/// RequestUrl::RequestUri { .. } => Err(()),
/// RequestUrl::RequestObject { .. } => Err(()),
/// }.is_ok());
/// ```
#[derive(Deserialize, Debug, PartialEq, Clone, Serialize)]
#[derive(Deserialize, Debug, PartialEq, Serialize, Clone)]
#[serde(untagged, deny_unknown_fields)]
pub enum RequestUrl {
AuthorizationRequest(Box<AuthorizationRequest>),
// TODO: Add client_id parameter.
RequestUri { request_uri: String },
Request(Box<AuthorizationRequest>),
RequestObject { client_id: String, request: String },
RequestUri { client_id: String, request_uri: String },
}

impl RequestUrl {
Expand All @@ -68,8 +71,9 @@ impl TryInto<AuthorizationRequest> for RequestUrl {

fn try_into(self) -> Result<AuthorizationRequest, Self::Error> {
match self {
RequestUrl::AuthorizationRequest(request) => Ok(*request),
RequestUrl::RequestUri { .. } => Err(anyhow!("AuthorizationRequest is a request URI.")),
RequestUrl::Request(request) => Ok(*request),
RequestUrl::RequestUri { .. } => Err(anyhow!("Request is a request URI.")),
RequestUrl::RequestObject { .. } => Err(anyhow!("Request is a request object.")),
}
}
}
Expand Down Expand Up @@ -127,9 +131,12 @@ pub enum ResponseType {

/// [`AuthorizationRequest`] is a request from a [crate::relying_party::RelyingParty] (RP) to a [crate::provider::Provider] (SIOP).
#[allow(dead_code)]
#[derive(Debug, Getters, PartialEq, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Getters, PartialEq, Default, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct AuthorizationRequest {
#[serde(flatten)]
#[getset(get = "pub")]
pub(super) rfc7519_claims: RFC7519Claims,
pub(crate) response_type: ResponseType,
pub(crate) response_mode: Option<String>,
#[getset(get = "pub")]
Expand All @@ -144,11 +151,6 @@ pub struct AuthorizationRequest {
pub(crate) nonce: String,
#[getset(get = "pub")]
pub(crate) registration: Option<Registration>,
pub(crate) iss: Option<String>,
pub(crate) iat: Option<i64>,
pub(crate) exp: Option<i64>,
pub(crate) nbf: Option<i64>,
pub(crate) jti: Option<String>,
#[getset(get = "pub")]
pub(crate) state: Option<String>,
}
Expand Down Expand Up @@ -183,11 +185,12 @@ mod tests {
#[test]
fn test_valid_request_uri() {
// A form urlencoded string with a `request_uri` parameter should deserialize into the `RequestUrl::RequestUri` variant.
let request_url = RequestUrl::from_str("siopv2://idtoken?request_uri=https://example.com/request_uri").unwrap();
let request_url = RequestUrl::from_str("siopv2://idtoken?client_id=https%3A%2F%2Fclient.example.org%2Fcb&request_uri=https://example.com/request_uri").unwrap();
assert_eq!(
request_url,
RequestUrl::RequestUri {
request_uri: "https://example.com/request_uri".to_string()
client_id: "https://client.example.org/cb".to_string(),
request_uri: "https://example.com/request_uri".to_string(),
}
);
}
Expand All @@ -212,7 +215,8 @@ mod tests {
.unwrap();
assert_eq!(
request_url.clone(),
RequestUrl::AuthorizationRequest(Box::new(AuthorizationRequest {
RequestUrl::Request(Box::new(AuthorizationRequest {
rfc7519_claims: RFC7519Claims::default(),
response_type: ResponseType::IdToken,
response_mode: Some("post".to_string()),
client_id: "did:example:\
Expand All @@ -227,11 +231,6 @@ mod tests {
.with_subject_syntax_types_supported(vec!["did:mock".to_string()])
.with_id_token_signing_alg_values_supported(vec!["EdDSA".to_string()]),
),
iss: None,
iat: None,
exp: None,
nbf: None,
jti: None,
state: None,
}))
);
Expand All @@ -242,6 +241,22 @@ mod tests {
);
}

#[test]
fn test_valid_request_object() {
// A form urlencoded string with a `request` parameter should deserialize into the `RequestUrl::RequestObject` variant.
let request_url = RequestUrl::from_str(
"siopv2://idtoken?client_id=https%3A%2F%2Fclient.example.org%2Fcb&request=eyJhb...lMGzw",
)
.unwrap();
assert_eq!(
request_url,
RequestUrl::RequestObject {
client_id: "https://client.example.org/cb".to_string(),
request: "eyJhb...lMGzw".to_string()
}
);
}

#[test]
fn test_invalid_request() {
// A form urlencoded string with an otherwise valid request is invalid when the `request_uri` parameter is also
Expand Down
91 changes: 43 additions & 48 deletions siopv2/src/request/request_builder.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,70 @@
use crate::{
builder_fn,
claims::ClaimRequests,
request::{AuthorizationRequest, RequestUrl, ResponseType},
token::id_token::RFC7519Claims,
Registration, Scope,
};
use anyhow::{anyhow, Result};
use is_empty::IsEmpty;

#[derive(Default, IsEmpty)]
pub struct RequestUrlBuilder {
rfc7519_claims: RFC7519Claims,
client_id: Option<String>,
request: Option<String>,
request_uri: Option<String>,
response_type: Option<ResponseType>,
response_mode: Option<String>,
client_id: Option<String>,
scope: Option<Scope>,
claims: Option<Result<ClaimRequests>>,
redirect_uri: Option<String>,
nonce: Option<String>,
registration: Option<Registration>,
iss: Option<String>,
iat: Option<i64>,
exp: Option<i64>,
nbf: Option<i64>,
jti: Option<String>,
state: Option<String>,
}

macro_rules! builder_fn {
($name:ident, $ty:ty) => {
pub fn $name(mut self, value: $ty) -> Self {
self.$name = Some(value);
self
}
};
}

impl RequestUrlBuilder {
pub fn new() -> Self {
RequestUrlBuilder::default()
}

pub fn build(&mut self) -> Result<RequestUrl> {
let request_uri = self.request_uri.take();
match (request_uri, self.is_empty()) {
(Some(request_uri), true) => Ok(RequestUrl::RequestUri { request_uri }),
(None, _) => Ok(RequestUrl::AuthorizationRequest(Box::new(AuthorizationRequest {
pub fn build(mut self) -> Result<RequestUrl> {
match (
self.client_id.take(),
self.request.take(),
self.request_uri.take(),
self.is_empty(),
) {
(None, _, _, _) => Err(anyhow!("client_id parameter is required.")),
(Some(client_id), Some(request), None, true) => Ok(RequestUrl::RequestObject { client_id, request }),
(Some(client_id), None, Some(request_uri), true) => Ok(RequestUrl::RequestUri { client_id, request_uri }),
(Some(client_id), None, None, false) => Ok(RequestUrl::Request(Box::new(AuthorizationRequest {
rfc7519_claims: self.rfc7519_claims,
client_id,
response_type: self
.response_type
.take()
.ok_or(anyhow!("response_type parameter is required."))?,
.ok_or_else(|| anyhow!("response_type parameter is required."))?,
response_mode: self.response_mode.take(),
client_id: self
.client_id
scope: self
.scope
.take()
.ok_or(anyhow!("client_id parameter is required."))?,
scope: self.scope.take().ok_or(anyhow!("scope parameter is required."))?,
.ok_or_else(|| anyhow!("scope parameter is required."))?,
claims: self.claims.take().transpose()?,
redirect_uri: self
.redirect_uri
.take()
.ok_or(anyhow!("redirect_uri parameter is required."))?,
nonce: self.nonce.take().ok_or(anyhow!("nonce parameter is required."))?,
.ok_or_else(|| anyhow!("redirect_uri parameter is required."))?,
nonce: self
.nonce
.take()
.ok_or_else(|| anyhow!("nonce parameter is required."))?,
registration: self.registration.take(),
iss: self.iss.take(),
iat: self.iat,
exp: self.exp,
nbf: self.nbf,
jti: self.jti.take(),
state: self.state.take(),
}))),
_ => Err(anyhow!(
"request_uri and other parameters cannot be set at the same time."
"one of either request_uri, request or other parameters should be set"
)),
}
}
Expand All @@ -79,6 +74,13 @@ impl RequestUrlBuilder {
self
}

builder_fn!(rfc7519_claims, iss, String);
builder_fn!(rfc7519_claims, sub, String);
builder_fn!(rfc7519_claims, aud, String);
builder_fn!(rfc7519_claims, exp, i64);
builder_fn!(rfc7519_claims, nbf, i64);
builder_fn!(rfc7519_claims, iat, i64);
builder_fn!(rfc7519_claims, jti, String);
builder_fn!(request_uri, String);
builder_fn!(response_type, ResponseType);
builder_fn!(response_mode, String);
Expand All @@ -87,11 +89,6 @@ impl RequestUrlBuilder {
builder_fn!(redirect_uri, String);
builder_fn!(nonce, String);
builder_fn!(registration, Registration);
builder_fn!(iss, String);
builder_fn!(iat, i64);
builder_fn!(exp, i64);
builder_fn!(nbf, i64);
builder_fn!(jti, String);
builder_fn!(state, String);
}

Expand Down Expand Up @@ -120,7 +117,8 @@ mod tests {

assert_eq!(
request_url,
RequestUrl::AuthorizationRequest(Box::new(AuthorizationRequest {
RequestUrl::Request(Box::new(AuthorizationRequest {
rfc7519_claims: RFC7519Claims::default(),
response_type: ResponseType::IdToken,
response_mode: None,
client_id: "did:example:123".to_string(),
Expand All @@ -135,11 +133,6 @@ mod tests {
redirect_uri: "https://example.com".to_string(),
nonce: "nonce".to_string(),
registration: None,
iss: None,
iat: None,
exp: None,
nbf: None,
jti: None,
state: None,
}))
);
Expand Down Expand Up @@ -167,10 +160,10 @@ mod tests {
.nonce("nonce".to_string())
.claims(
r#"{
"id_token": {
"name": "invalid"
}
}"#,
"id_token": {
"name": "invalid"
}
}"#,
)
.build()
.is_err());
Expand All @@ -179,13 +172,15 @@ mod tests {
#[test]
fn test_valid_request_uri_builder() {
let request_url = RequestUrl::builder()
.client_id("did:example:123".to_string())
.request_uri("https://example.com/request_uri".to_string())
.build()
.unwrap();

assert_eq!(
request_url,
RequestUrl::RequestUri {
client_id: "did:example:123".to_string(),
request_uri: "https://example.com/request_uri".to_string()
}
);
Expand Down
Loading

0 comments on commit a81b8c2

Please sign in to comment.