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

feat: improved builder pattern for RequestUrl #27

Merged
merged 35 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
55a58e1
Add support for Request by reference
nanderstabel Apr 14, 2023
267fabc
fix: remove custom serde
nanderstabel Apr 19, 2023
c314960
Add claims and scope parameters
nanderstabel Apr 19, 2023
72b3a13
Add Storage and RelyingParty test improvement
nanderstabel Apr 19, 2023
46b6057
Update README example
nanderstabel Apr 19, 2023
d4a915d
fix: Add standard_claims to test IdToken
nanderstabel Apr 19, 2023
8b6a843
Move Storage trait to test_utils
nanderstabel Apr 19, 2023
6009866
Remove storage.rs
nanderstabel Apr 19, 2023
19c4229
fix: fex rebase to dev
nanderstabel Apr 25, 2023
57c5047
fix: fix rebase to dev
nanderstabel Apr 25, 2023
89ff741
feat: add Claim trait with associated types
nanderstabel May 12, 2023
0e8f50e
fix: build
nanderstabel May 23, 2023
25c7737
fix: remove build.rs and change crate name in doc tests
nanderstabel May 24, 2023
0d31ab6
feat: refactor claims.rs
nanderstabel May 30, 2023
466d57b
fix: remove skeptic crate
nanderstabel Apr 25, 2023
fe35082
feat: allow json arguments for claims() method
nanderstabel May 12, 2023
306e2df
style: add specific request folder
nanderstabel May 26, 2023
97f7a86
test: improve RequestBuilder tests
nanderstabel May 31, 2023
dc2a34e
fix: fix rebase
nanderstabel Jun 5, 2023
0156399
Add support for Request by reference
nanderstabel Apr 14, 2023
419f717
fix: remove custom serde
nanderstabel Apr 19, 2023
5760861
Add claims and scope parameters
nanderstabel Apr 19, 2023
0883995
Add Storage and RelyingParty test improvement
nanderstabel Apr 19, 2023
ddc4829
Update README example
nanderstabel Apr 19, 2023
3dd4593
fix: Add standard_claims to test IdToken
nanderstabel Apr 19, 2023
17b9849
Move Storage trait to test_utils
nanderstabel Apr 19, 2023
cc3aa20
fix: fex rebase to dev
nanderstabel Apr 25, 2023
099824e
feat: add Claim trait with associated types
nanderstabel May 12, 2023
997cf35
feat: refactor claims.rs
nanderstabel May 30, 2023
d3d8623
feat: Add builder for Response and IdToken
nanderstabel May 24, 2023
6d48774
feat: add missing ID Token claim parameters
nanderstabel May 30, 2023
310a581
style: rename request and response
nanderstabel May 31, 2023
1c703e4
feat: improve request builder
nanderstabel May 31, 2023
4863f63
fix: fix rebase
nanderstabel Jun 5, 2023
c2d01e0
fix: fix doctest
nanderstabel Jun 5, 2023
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
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