#open-id

siopv2

Rust implementation for the OpenID Connect Self-Issued OpenID Provider v2 standard

1 unstable release

0.1.0 Jun 12, 2023

#14 in #open-id


Used in oid4vc

Apache-2.0

97KB
2K SLoC

openid4vc

This library aims to support all the specifications under the OpenID for Verifiable Credentials works.

OpenID for Verifiable Credentials (OID4VC) consists of the following specifications:

Description

Currently the Implicit Flow is consists of four major parts:

  • A Provider that can accept a AuthorizationRequest and generate a AuthorizationResponse by creating an IdToken, adding its key identifier to the header of the id_token, signing the id_token and wrap it into a AuthorizationResponse. It can also send the AuthorizationResponse using the redirect_uri parameter.
  • A RelyingParty struct which can validate a AuthorizationResponse by validating its IdToken using a key identifier (which is extracted from the id_token) and its public key.
  • The Subject trait can be implemented on a custom struct representing the signing logic of a DID method. A Provider can ingest an object that implements the Subject trait so that during generation of a AuthorizationResponse the DID method syntax, key identifier and signing method of the specific Subject can be used.
  • The Validator trait can be implemented on a custom struct representing the validating logic of a DID method. When ingested by a RelyingParty, it can resolve the public key that is needed for validating an IdToken.

Example

use anyhow::Result;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use ed25519_dalek::{Keypair, Signature, Signer};
use lazy_static::lazy_static;
use siopv2::{
    claims::{ClaimRequests, ClaimValue, IndividualClaimRequest},
    request::ResponseType,
    Provider, Registration, RelyingParty, RequestUrl, AuthorizationResponse, Scope, AuthorizationRequest, StandardClaims, Subject, Validator,
};
use rand::rngs::OsRng;
use wiremock::{
    http::Method,
    matchers::{method, path},
    Mock, MockServer, ResponseTemplate,
};

lazy_static! {
    pub static ref MOCK_KEYPAIR: Keypair = Keypair::generate(&mut OsRng);
}

// A Subject type that can be ingested by a Provider
#[derive(Default)]
pub struct MySubject;

impl MySubject {
    pub fn new() -> Self {
        MySubject {}
    }
}

#[async_trait]
impl Subject for MySubject {
    fn did(&self) -> Result<did_url::DID> {
        Ok(did_url::DID::parse("did:mymethod:subject")?)
    }

    fn key_identifier(&self) -> Option<String> {
        Some("key_identifier".to_string())
    }

    async fn sign<'a>(&self, message: &'a str) -> Result<Vec<u8>> {
        let signature: Signature = MOCK_KEYPAIR.sign(message.as_bytes());
        Ok(signature.to_bytes().to_vec())
    }
}

#[async_trait]
impl Validator for MySubject {
    async fn public_key<'a>(&self, _kid: &'a str) -> Result<Vec<u8>> {
        Ok(MOCK_KEYPAIR.public.to_bytes().to_vec())
    }
}

// A Validator type that can be ingested by a RelyingParty
#[derive(Default)]
pub struct MyValidator;

#[async_trait]
impl Validator for MyValidator {
    async fn public_key<'a>(&self, _kid: &'a str) -> Result<Vec<u8>> {
        Ok(MOCK_KEYPAIR.public.to_bytes().to_vec())
    }
}

#[tokio::main]
async fn main() {
    // Create a new mock server and retreive it's url.
    let mock_server = MockServer::start().await;
    let server_url = mock_server.uri();

    // Create a new validator.
    let validator = MySubject::default();

    // Create a new relying party.
    let relying_party = RelyingParty::new(validator);

    // Create a new RequestUrl with response mode `post` for cross-device communication.
    let request: AuthorizationRequest = RequestUrl::builder()
        .response_type(ResponseType::IdToken)
        .client_id("did:mymethod:relyingparty".to_string())
        .scope(Scope::openid())
        .redirect_uri(format!("{server_url}/redirect_uri"))
        .response_mode("post".to_string())
        .registration(
            Registration::default()
                .with_subject_syntax_types_supported(vec!["did:mymethod".to_string()])
                .with_id_token_signing_alg_values_supported(vec!["EdDSA".to_string()]),
        )
        .claims(ClaimRequests {
            id_token: Some(StandardClaims {
                name: Some(IndividualClaimRequest::default()),
                ..Default::default()
            }),
            ..Default::default()
        })
        .exp((Utc::now() + Duration::minutes(10)).timestamp())
        .nonce("n-0S6_WzA2Mj".to_string())
        .build()
        .and_then(TryInto::try_into)
        .unwrap();

    // Create a new `request_uri` endpoint on the mock server and load it with the JWT encoded `AuthorizationRequest`.
    Mock::given(method("GET"))
        .and(path("/request_uri"))
        .respond_with(ResponseTemplate::new(200).set_body_string(relying_party.encode(&request).await.unwrap()))
        .mount(&mock_server)
        .await;

    // Create a new `redirect_uri` endpoint on the mock server where the `Provider` will send the `AuthorizationResponse`.
    Mock::given(method("POST"))
        .and(path("/redirect_uri"))
        .respond_with(ResponseTemplate::new(200))
        .mount(&mock_server)
        .await;

    // Create a new subject.
    let subject = MySubject::default();

    // Create a new provider.
    let provider = Provider::new(subject).await.unwrap();

    // Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint.
    let request_url = RequestUrl::builder()
        .request_uri(format!("{server_url}/request_uri"))
        .build()
        .unwrap();

    // The Provider obtains the reuquest url either by a deeplink or by scanning a QR code. It then validates the
    // request. Since in this case the request is a JWT, the provider will fetch the request by sending a GET
    // request to mock server's `request_uri` endpoint.
    let request = provider.validate_request(request_url).await.unwrap();

    // Assert that the request was successfully received by the mock server at the `request_uri` endpoint.
    let get_request = mock_server.received_requests().await.unwrap()[0].clone();
    assert_eq!(get_request.method, Method::Get);
    assert_eq!(get_request.url.path(), "/request_uri");

    // Let the provider generate a response based on the validated request. The response is an `IdToken` which is
    // encoded as a JWT.
    let response = provider
        .generate_response(
            request,
            StandardClaims {
                name: Some(ClaimValue("Jane Doe".to_string())),
                ..Default::default()
            },
        )
        .await
        .unwrap();

    // The provider sends it's response to the mock server's `redirect_uri` endpoint.
    provider.send_response(response).await.unwrap();

    // Assert that the AuthorizationResponse was successfully received by the mock server at the expected endpoint.
    let post_request = mock_server.received_requests().await.unwrap()[1].clone();
    assert_eq!(post_request.method, Method::Post);
    assert_eq!(post_request.url.path(), "/redirect_uri");
    let response: AuthorizationResponse = serde_urlencoded::from_bytes(post_request.body.as_slice()).unwrap();

    // The `RelyingParty` then validates the response by decoding the header of the id_token, by fetching the public
    // key corresponding to the key identifier and finally decoding the id_token using the public key and by
    // validating the signature.
    let id_token = relying_party.validate_response(&response).await.unwrap();
    assert_eq!(
        id_token.standard_claims(),
        &StandardClaims {
            name: Some(ClaimValue("Jane Doe".to_string())),
            ..Default::default()
        }
    );
}

Dependencies

~20–33MB
~607K SLoC