Skip to content

Commit

Permalink
feat: improve request builder
Browse files Browse the repository at this point in the history
  • Loading branch information
nanderstabel committed Jun 5, 2023
1 parent 74a7eb0 commit f44f865
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 92 deletions.
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ pub use jwt::JsonWebToken;
pub use provider::Provider;
pub use registration::Registration;
pub use relying_party::RelyingParty;
pub use request::{request_builder::RequestUrlBuilder, Request, RequestUrl};
pub use response::Response;
pub use request::{request_builder::RequestUrlBuilder, AuthorizationRequest, RequestUrl};
pub use response::AuthorizationResponse;
pub use scope::Scope;
pub use subject::Subject;
pub use token::{id_token::IdToken, id_token_builder::IdTokenBuilder};
Expand Down
46 changes: 30 additions & 16 deletions src/provider.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::{IdToken, Request, RequestUrl, Response, StandardClaimsValues, Subject, Validator};
use crate::{
AuthorizationRequest, AuthorizationResponse, IdToken, RequestUrl, StandardClaimsValues, Subject, Validator,
};
use anyhow::{anyhow, Result};
use chrono::{Duration, Utc};

/// A Self-Issued OpenID Provider (SIOP), which is responsible for generating and signing [`IdToken`]'s in response to
/// [`Request`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and
/// [`AuthorizationRequest`]'s from [crate::relying_party::RelyingParty]'s (RPs). The [`Provider`] acts as a trusted intermediary between the RPs and
/// the user who is trying to authenticate.
#[derive(Default)]
pub struct Provider<S>
Expand All @@ -27,35 +29,47 @@ where
}

/// TODO: Add more validation rules.
/// Takes a [`RequestUrl`] and returns a [`Request`]. The [`RequestUrl`] can either be a [`Request`] or a
/// Takes a [`RequestUrl`] and returns a [`AuthorizationRequest`]. The [`RequestUrl`] can either be a [`AuthorizationRequest`] or a
/// 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<Request> {
let request = match request {
RequestUrl::Request(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?
}
pub async fn validate_request(&self, request: RequestUrl) -> Result<AuthorizationRequest> {
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()),
)
},
)
})
}

/// Generates a [`Response`] in response to a [`Request`] and the user's claims. The [`Response`]
/// Generates a [`AuthorizationResponse`] in response to a [`AuthorizationRequest`] and the user's claims. The [`AuthorizationResponse`]
/// contains an [`IdToken`], which is signed by the [`Subject`] of the [`Provider`].
pub async fn generate_response(&self, request: Request, user_claims: StandardClaimsValues) -> Result<Response> {
pub async fn generate_response(
&self,
request: AuthorizationRequest,
user_claims: StandardClaimsValues,
) -> Result<AuthorizationResponse> {
let subject_did = self.subject.did()?;

let id_token = IdToken::builder()
Expand Down
5 changes: 3 additions & 2 deletions src/relying_party.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{IdToken, Request, Response, Subject, Validator};
use crate::{AuthorizationRequest, AuthorizationResponse, IdToken, Subject, Validator};
use anyhow::Result;

pub struct RelyingParty<V>
Expand Down Expand Up @@ -42,7 +42,7 @@ mod tests {
request::ResponseType,
scope::{Scope, ScopeValue},
test_utils::{MemoryStorage, MockSubject, Storage},
Provider, Registration, RequestUrl, StandardClaimsRequests, StandardClaimsValues,
ClaimRequests, Provider, Registration, RequestUrl, StandardClaimsRequests, StandardClaimsValues,
};
use chrono::{Duration, Utc};
use lazy_static::lazy_static;
Expand Down Expand Up @@ -156,6 +156,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
49 changes: 32 additions & 17 deletions 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 @@ -47,14 +49,15 @@ pub mod request_builder;
/// assert!(match request_url {
/// 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 {
Request(Box<AuthorizationRequest>),
// TODO: Add client_id parameter.
RequestUri { request_uri: String },
RequestObject { client_id: String, request: String },
RequestUri { client_id: String, request_uri: String },
}

impl RequestUrl {
Expand All @@ -70,6 +73,7 @@ impl TryInto<AuthorizationRequest> for RequestUrl {
match self {
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 @@ -213,6 +216,7 @@ mod tests {
assert_eq!(
request_url.clone(),
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
Loading

0 comments on commit f44f865

Please sign in to comment.