Skip to content

Commit

Permalink
[FIX] Remove extraneous quotations around url encoded vp_token (#54)
Browse files Browse the repository at this point in the history
* strip extra quotations in authorization response url encoding, add optional state property to response

Signed-off-by: Ryan Tate <[email protected]>

* add should_strip_quotes bool option to auth response

This will remove quotations around the `vp_token`
url-encoded parameter, i.e. %22

Signed-off-by: Ryan Tate <[email protected]>

---------

Signed-off-by: Ryan Tate <[email protected]>
  • Loading branch information
Ryanmtate authored Feb 18, 2025
1 parent 8df92db commit 9d79eb5
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 10 deletions.
8 changes: 7 additions & 1 deletion src/core/authorization_request/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::ops::{Deref, DerefMut};

use anyhow::{anyhow, bail, Context, Error, Result};
use parameters::ClientMetadata;
use parameters::{ClientMetadata, State};
use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use url::Url;
Expand Down Expand Up @@ -280,6 +280,12 @@ impl AuthorizationRequestObject {
.get()
.ok_or(anyhow!("missing vp_formats"))?
}

/// Return the `state` of the authorization request,
/// if it was provided.
pub fn state(&self) -> Option<Result<State>> {
self.0.get()
}
}

impl From<AuthorizationRequestObject> for UntypedObject {
Expand Down
2 changes: 1 addition & 1 deletion src/core/authorization_request/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ impl From<ResponseType> for Json {
}
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct State(pub String);

impl TypedParameter for State {
Expand Down
1 change: 1 addition & 0 deletions src/core/presentation_submission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ pub struct DescriptorMap {
pub id: DescriptorMapId,
pub format: ClaimFormatDesignation,
pub path: JsonPath,
#[serde(skip_serializing_if = "Option::is_none")]
pub path_nested: Option<Box<DescriptorMap>>,
}

Expand Down
65 changes: 58 additions & 7 deletions src/core/response/mod.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
use super::{object::UntypedObject, presentation_submission::PresentationSubmission};

use anyhow::{Context, Error, Result};
use parameters::State;
use serde::{Deserialize, Serialize};
use url::Url;

use self::parameters::VpToken;

pub mod parameters;

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AuthorizationResponse {
Unencoded(UnencodedAuthorizationResponse),
Jwt(JwtAuthorizationResponse),
}

impl AuthorizationResponse {
pub fn from_x_www_form_urlencoded(bytes: &[u8]) -> Result<Self> {
pub fn from_x_www_form_urlencoded(bytes: &[u8], should_strip_quotes: bool) -> Result<Self> {
if let Ok(jwt) = serde_urlencoded::from_bytes(bytes) {
return Ok(Self::Jwt(jwt));
}
Expand All @@ -30,25 +32,43 @@ impl AuthorizationResponse {
serde_json::from_str(&unencoded.presentation_submission)
.context("failed to decode presentation submission")?;

let state: Option<State> = unencoded
.state
.map(|s| serde_json::from_str(&s))
.transpose()
.context("failed to decode state")?;

Ok(Self::Unencoded(UnencodedAuthorizationResponse {
vp_token,
presentation_submission,
state,
should_strip_quotes,
}))
}
}

#[derive(Debug, Deserialize, Serialize)]
struct JsonEncodedAuthorizationResponse {
/// `vp_token` is JSON string encoded.
pub(crate) vp_token: String,
/// `presentation_submission` is JSON string encoded.
pub(crate) presentation_submission: String,
/// `vp_token` is JSON string encoded.
pub(crate) vp_token: String,
/// `state` is a regular string.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) state: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UnencodedAuthorizationResponse {
pub vp_token: VpToken,
pub presentation_submission: PresentationSubmission,
pub vp_token: VpToken,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<State>,
/// This is an internal non-normative setting to determine
/// the behavior of removing extra quotations around a JSON
/// string encoded vp_token, e.g. "'[{ @context: [...] }]'" -> '[{ @context: [...] }]'
#[serde(skip)]
pub should_strip_quotes: bool,
}

impl UnencodedAuthorizationResponse {
Expand All @@ -71,20 +91,44 @@ impl UnencodedAuthorizationResponse {
pub fn presentation_submission(&self) -> &PresentationSubmission {
&self.presentation_submission
}

// Internal helper method for cleaning up quoted strings.
// urlencoding a JSON string adds quotes around the string,
// which causes issues when decoding for some verifiers.
fn sanitize_string(&self, s: String) -> String {
// NOTE: Depending on whether `should_strip_quotes` option is set,
// this will remove any extra quotations around a JSON string.
match self.should_strip_quotes {
true => s.trim_matches(|c| c == '"' || c == '\'').to_string(),
false => s,
}
}
}

impl From<UnencodedAuthorizationResponse> for JsonEncodedAuthorizationResponse {
fn from(value: UnencodedAuthorizationResponse) -> Self {
let vp_token = serde_json::to_string(&value.vp_token)
.ok()
// Perform any optional string sanitizations
.map(|s| value.sanitize_string(s))
// SAFTEY: VP Token will always be a valid JSON object.
.unwrap();

let presentation_submission = serde_json::to_string(&value.presentation_submission)
// SAFETY: presentation submission will always be a valid JSON object.
.unwrap();

let state = value
.state
.map(|s| serde_json::to_string(&s))
.transpose()
// SAFETY: State will always be a valid JSON object.
.unwrap();

Self {
vp_token,
presentation_submission,
state,
}
}
}
Expand Down Expand Up @@ -148,10 +192,17 @@ mod test {
}
))
.unwrap();
let response = UnencodedAuthorizationResponse::try_from(object).unwrap();
let url_encoded = response.into_x_www_form_urlencoded().unwrap();
let mut response = UnencodedAuthorizationResponse::try_from(object).unwrap();

let url_encoded = response.clone().into_x_www_form_urlencoded().unwrap();
assert!(url_encoded.contains("presentation_submission=%7B%22id%22%3A%22d05a7f51-ac09-43af-8864-e00f0175f2c7%22%2C%22definition_id%22%3A%22f619e64a-8f80-4b71-8373-30cf07b1e4f2%22%2C%22descriptor_map%22%3A%5B%5D%7D"));
assert!(url_encoded.contains("vp_token=%22string%22"));

// NOTE: when `should_strip_quotes` is true, the `%22`
// quote encodings enclosing the `vp_token` parameter are stripped
response.should_strip_quotes = true;

let url_encoded = response.into_x_www_form_urlencoded().unwrap();
assert!(url_encoded.contains("vp_token=string"));
}
}
2 changes: 2 additions & 0 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ async fn w3c_vc_did_client_direct_post() {
let response = AuthorizationResponse::Unencoded(UnencodedAuthorizationResponse {
vp_token: vp.into(),
presentation_submission: presentation_submission.try_into().unwrap(),
state: None,
should_strip_quotes: false,
});

let status = verifier.poll_status(id).await.unwrap();
Expand Down
2 changes: 1 addition & 1 deletion tests/jwt_vc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ impl AsyncHttpClient for MockHttpClient {
self.verifier
.verify_response(
id.parse().context("failed to parse id")?,
AuthorizationResponse::from_x_www_form_urlencoded(body)
AuthorizationResponse::from_x_www_form_urlencoded(body, false)
.context("failed to parse authorization response request")?,
|_, _| {
Box::pin(async move {
Expand Down

0 comments on commit 9d79eb5

Please sign in to comment.