Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
10 changes: 5 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ rust-version = "1.76.0"
pedantic = "warn"

[workspace.dependencies]
siopv2 = { git = "https://github.com/impierce/openid4vc", rev = "e860178" }
oid4vci = { git = "https://github.com/impierce/openid4vc", rev = "e860178" }
oid4vc-core = { git = "https://github.com/impierce/openid4vc", rev = "e860178" }
oid4vc-manager = { git = "https://github.com/impierce/openid4vc", rev = "e860178" }
oid4vp = { git = "https://github.com/impierce/openid4vc", rev = "e860178" }
siopv2 = { git = "https://github.com/impierce/openid4vc", rev = "dc89ac2" }
oid4vci = { git = "https://github.com/impierce/openid4vc", rev = "dc89ac2" }
oid4vc-core = { git = "https://github.com/impierce/openid4vc", rev = "dc89ac2" }
oid4vc-manager = { git = "https://github.com/impierce/openid4vc", rev = "dc89ac2" }
oid4vp = { git = "https://github.com/impierce/openid4vc", rev = "dc89ac2" }

shared-kernel = { path = "shared-kernel" }

Expand Down
16 changes: 14 additions & 2 deletions agent_api_http/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,21 @@ async fn buffer_request_body(request: Request) -> Result<Request, Response> {
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()).into_response())?
.to_bytes();

let _ = serde_json::from_slice(&bytes)
// Print pretty JSON when possible, otherwise log the raw body.
if let Ok(pretty_json) = serde_json::from_slice(&bytes)
.and_then(|json_value: serde_json::Value| serde_json::to_string_pretty(&json_value))
.map(|pretty_json| info!("Request Body: {}", pretty_json));
{
info!("Request Body JSON: {}", pretty_json);
} else {
let content_type = parts
.headers
.get(http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.unwrap_or("unknown");

let raw_body = String::from_utf8_lossy(&bytes);
info!(%content_type, body = %raw_body, "Request Body raw");
}

Ok(Request::from_parts(parts, Body::from(bytes)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use axum::{
response::{IntoResponse, Response},
};
use oid4vci::authorization_request::AuthorizationRequest;
use tracing::error;
use std::sync::Arc;

#[axum_macros::debug_handler]
Expand All @@ -18,7 +19,10 @@ pub(crate) async fn par(
PushedAuthorizationService::handle_pushed_authorization_request(&state, pushed_authorization_request)
.await
// TODO: implement proper error handling
.map_err(|_err| PublicError::InternalServerError)?;
.map_err(|err| {
error!("Error handling pushed authorization request: {:?}", err);
PublicError::InternalServerError
})?;

Ok((StatusCode::CREATED, Json(pushed_authorization_response)).into_response())
}
Expand Down Expand Up @@ -72,13 +76,13 @@ pub mod tests {
code_challenge_method: Some(CodeChallengeMethod::S256),
scope: Some("openid profile".to_string()),
issuer_state: Some(issuer_state),
authorization_details: vec![AuthorizationDetailsObject {
authorization_details: Some(vec![AuthorizationDetailsObject {
r#type: OpenidCredential::Type,
locations: None,
credential_configuration_id: "configuration_id".to_string(),
credential_identifiers: None,
claims: None,
}],
}]),
}))
.unwrap(),
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use oid4vci::authorization_details::AuthorizationDetailsObject;
pub struct ConsentPageTemplate {
pub client_name: String,
pub client_id: String,
pub authorization_details: Vec<AuthorizationDetailsObject>,
pub authorization_details: Option<Vec<AuthorizationDetailsObject>>,
pub request_uri: String,
}

Expand Down
10 changes: 8 additions & 2 deletions agent_api_http/src/v0/issuance/credential_issuer/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use agent_issuance::{
nonce_validation_service::NonceValidationService,
},
credential::{command::CredentialCommand, views::CredentialView},
offer::{command::OfferCommand, views::OfferView},
offer::{aggregate::ISSUER_STATE_PREFIX, command::OfferCommand, views::OfferView},
server_config::views::ServerConfigView,
state::{IssuanceState, SERVER_CONFIG_ID},
status_list::command::StatusListCommand,
Expand Down Expand Up @@ -44,10 +44,16 @@ pub(crate) async fn credential(
let claims = AccessTokenValidationService::validate(&state, &access_token).await?;

// The Access Token must contain the `issuer_state` claim, which is used to identify the `offer_id`.
let offer_id = claims
let issuer_state = claims
.issuer_state
.ok_or_else(|| PublicError::from(AccessTokenValidationError::InvalidToken))?;

// Here we strip the issuer_state_prefix which is added to ensure that also numbers are parsed as a String.
let offer_id = issuer_state
.strip_prefix(ISSUER_STATE_PREFIX)
.unwrap_or(issuer_state.as_str())
.to_string();

NonceValidationService::validate(&state, &credential_request)
.await
.map_err(|_| PublicError::from(CredentialErrorResponse::InvalidNonce))?;
Expand Down
20 changes: 13 additions & 7 deletions agent_api_http/templates/consent.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,20 +134,26 @@ <h1 style="text-align: center">Consent to Data Access</h1>
style="height: 100%"
/>
</div>
{% if let Some(auth_details_list) = authorization_details %}
<p>
The application <strong>{{ client_id }}</strong> is requesting access to
the following data:
</p>
<h2>Credentials to be Shared</h2>
<!-- TODO: In the future, rely on either the initial credential_configuration_ids or the auth_details (as we do below) to populate the "to be shared" credentials.
We currently do not support using the scope parameter for this. More info here: https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-final.html#name-identifying-credentials-bei -->
<ul>
{% for authorization_detail in authorization_details %}
<li>
<strong>{{ authorization_detail.credential_configuration_id }}</strong>
{% if let Some(claims) = &authorization_detail.claims %}
{% endif %}
</li>
{% endfor %}
{% for authorization_detail in auth_details_list %}
<li>
<strong>{{ authorization_detail.credential_configuration_id }}</strong>
</li>
{% endfor %}
</ul>
{% else %}
<p>
The application <strong>{{ client_id }}</strong> is requesting access to your data.
</p>
{% endif %}
<p>Do you want to allow this application to access your data?</p>
<form
action="/auth/consent"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use thiserror::Error;
pub struct ConsentPageViewModel {
pub client_id: String,
pub client_name: String,
pub authorization_details: Vec<AuthorizationDetailsObject>,
pub authorization_details: Option<Vec<AuthorizationDetailsObject>>,
pub request_uri: String,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub struct OAuth2AuthorizationRequest {
pub issuer_state: Option<String>,

// OID4VCI
pub authorization_details: Vec<AuthorizationDetailsObject>,
pub authorization_details: Option<Vec<AuthorizationDetailsObject>>,

// PKCE
#[serde(default)]
Expand Down Expand Up @@ -281,14 +281,14 @@ pub mod test_utils {
}

#[fixture]
pub fn authorization_details() -> Vec<AuthorizationDetailsObject> {
vec![AuthorizationDetailsObject {
pub fn authorization_details() -> Option<Vec<AuthorizationDetailsObject>> {
Some(vec![AuthorizationDetailsObject {
r#type: OpenidCredential::Type,
locations: None,
credential_configuration_id: "001".to_string(),
credential_identifiers: None,
claims: None,
}]
}])
}

static CODE_VERIFIER: OnceLock<Vec<u8>> = OnceLock::new();
Expand Down Expand Up @@ -325,7 +325,7 @@ pub mod test_utils {
redirect_uri: Option<Url>,
scope: String,
issuer_state: Option<String>,
authorization_details: Vec<AuthorizationDetailsObject>,
authorization_details: Option<Vec<AuthorizationDetailsObject>>,
code_challenge: String,
code_challenge_method: Option<CodeChallengeMethod>,
) -> AuthorizationRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub enum OAuth2AuthorizationRequestEvent {
issuer_state: Option<String>,

// OID4VCI
authorization_details: Vec<AuthorizationDetailsObject>,
authorization_details: Option<Vec<AuthorizationDetailsObject>>,

// PKCE
#[serde(default)]
Expand Down
6 changes: 5 additions & 1 deletion agent_issuance/src/offer/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use crate::utils::generate_tx_code::generate_tx_code;
use oid4vci::credential_offer::CredentialConfigurationIds;
use oid4vci::credential_request::CredentialIdentifierOrCredentialConfigurationId;

pub const ISSUER_STATE_PREFIX: &str = "issuer_state:";

#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq, utoipa::ToSchema)]
#[schema(as = CredentialOfferStatus)]
pub enum Status {
Expand Down Expand Up @@ -111,7 +113,9 @@ impl Aggregate for Offer {
let grants = Grants {
authorization_code: grant_types.contains(&GrantType::AuthorizationCode).then(|| {
AuthorizationCode {
issuer_state: Some(offer_id.clone()),
// We prefix the issuer_state here with a string to ensure it will always be parsed as a string, as per the spec https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-parameters.
// The offer_id is set by the user of our API, not by ourselves, it therefore can easily be a number which would then be parsed as such, consequently resulting in an error at the `credential` endpoint.
Comment on lines +116 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I suggest a (imo) slightly more explicit explanation as to why this prefix is necessary:

                            // Prefix `issuer_state` because it is later transported in an `application/x-www-form-urlencoded` request.
                            // Form values are strings, but some parsers coerce numeric-looking values (for example "001") to numbers.
                            // `offer_id` is client-provided and may look numeric, so this prefix preserves string semantics and avoids
                            // parsing ambiguity at the credential endpoint.

issuer_state: Some(format!("{}{}", ISSUER_STATE_PREFIX, offer_id)),
authorization_server: None,
}
}),
Expand Down
Loading