openid-client 0.2.7

OpenID client for Rust
Documentation
use assert_json_diff::assert_json_include;
use serde_json::json;

use crate::{
    client::Client,
    helpers::{decode_jwt, form_url_encoded_to_string_map},
    issuer::Issuer,
    tokenset::{TokenSet, TokenSetParams},
    types::{ClientMetadata, HttpMethod, IssuerMetadata, MtlsEndpoints, UserinfoOptions},
};

use crate::tests::test_http_client::TestHttpReqRes;

fn get_clients() -> (Client, Client) {
    let issuer_metadata = IssuerMetadata {
        issuer: "https://op.example.com".to_string(),
        userinfo_endpoint: Some("https://op.example.com/me".to_string()),
        token_endpoint: Some("https://op.example.com/token".to_string()),
        introspection_endpoint: Some("https://op.example.com/token/introspect".to_string()),
        revocation_endpoint: Some("https://op.example.com/token/revoke".to_string()),
        mtls_endpoint_aliases: Some(MtlsEndpoints {
            userinfo_endpoint: Some("https://mtls.op.example.com/me".to_string()),
            token_endpoint: Some("https://mtls.op.example.com/token".to_string()),
            introspection_endpoint: Some(
                "https://mtls.op.example.com/token/introspect".to_string(),
            ),
            revocation_endpoint: Some("https://mtls.op.example.com/token/revoke".to_string()),
            ..Default::default()
        }),
        ..Default::default()
    };

    let issuer = Issuer::new(issuer_metadata);

    let client_metadata = ClientMetadata {
        client_id: Some("client".to_string()),
        token_endpoint_auth_method: Some("self_signed_tls_client_auth".to_string()),
        tls_client_certificate_bound_access_tokens: Some(true),
        ..Default::default()
    };

    let client = issuer.client(client_metadata, None, None, None).unwrap();

    let jwt_client_metadata = ClientMetadata {
        client_id: Some("client".to_string()),
        client_secret: Some("abcdefghijklmnopqrstuvwxyz123456".to_string()),
        token_endpoint_auth_method: Some("client_secret_jwt".to_string()),
        token_endpoint_auth_signing_alg: Some("HS256".to_string()),
        tls_client_certificate_bound_access_tokens: Some(true),
        ..Default::default()
    };

    let jwt_auth_client = issuer
        .client(jwt_client_metadata, None, None, None)
        .unwrap();

    (client, jwt_auth_client)
}

#[tokio::test]
async fn uses_the_issuer_identifier_and_token_endpoint_as_private_key_jwt_audiences() {
    let (_, client) = get_clients();

    let req_token = client.auth_for("token", None).unwrap();
    let form_token = req_token
        .body
        .map(|b| form_url_encoded_to_string_map(&b))
        .unwrap();

    let decoded_token = decode_jwt(&form_token.get("client_assertion").unwrap()).unwrap();

    let aud_token = decoded_token.payload.claim("aud").unwrap();

    assert_json_include!(
        expected: json!(["https://op.example.com", "https://op.example.com/token"]),
        actual: aud_token
    );

    let req_introspection = client.auth_for("introspection", None).unwrap();
    let form_introspection = req_introspection
        .body
        .map(|b| form_url_encoded_to_string_map(&b))
        .unwrap();

    let decoded_introspection =
        decode_jwt(&form_introspection.get("client_assertion").unwrap()).unwrap();

    let aud_introspection = decoded_introspection.payload.claim("aud").unwrap();

    assert_json_include!(
        expected: json!(["https://op.example.com", "https://op.example.com/token"]),
        actual: aud_introspection
    );

    let req_revocation = client.auth_for("introspection", None).unwrap();
    let form_revocation = req_revocation
        .body
        .map(|b| form_url_encoded_to_string_map(&b))
        .unwrap();

    let decoded_revocation = decode_jwt(&form_revocation.get("client_assertion").unwrap()).unwrap();

    let aud_revocation = decoded_revocation.payload.claim("aud").unwrap();

    assert_json_include!(
        expected: json!(["https://op.example.com", "https://op.example.com/token"]),
        actual: aud_revocation
    );
}

#[tokio::test]
async fn requires_mtls_for_userinfo_when_tls_client_certificate_bound_access_tokens_is_true() {
    let mut http_client = TestHttpReqRes::new("https://mtls.op.example.com/me")
        .assert_request_method(HttpMethod::GET)
        .assert_request_header("accept", vec!["application/json".to_string()])
        .assert_request_header("authorization", vec!["Bearer foo".to_string()])
        .assert_request_mtls(true)
        .set_response_body(r#"{"sub":"foo"}"#)
        .set_response_status_code(200)
        .build();

    let (mut client, _) = get_clients();

    let token_params = TokenSetParams {
        access_token: Some("foo".to_string()),
        ..Default::default()
    };

    let token = TokenSet::new(token_params);

    http_client.return_client_cert(true);

    client
        .userinfo_async(&http_client, &token, UserinfoOptions::default())
        .await
        .unwrap();

    http_client.return_client_cert(false);

    let err = client
        .userinfo_async(&http_client, &token, UserinfoOptions::default())
        .await
        .unwrap_err();

    assert!(err.is_type_error());
    assert_eq!(
        "mutual-TLS certificate and key not set",
        err.type_error().error.message
    );
}

#[tokio::test]
async fn requires_mtls_for_introspection_authentication_when_introspection_endpoint_auth_method_is_tls_client_auth(
) {
    let mut http_client = TestHttpReqRes::new("https://mtls.op.example.com/token/introspect")
        .assert_request_method(HttpMethod::POST)
        .assert_request_header("accept", vec!["application/json".to_string()])
        .assert_request_header(
            "content-type",
            vec!["application/x-www-form-urlencoded".to_string()],
        )
        .assert_request_header("content-length", vec!["26".to_string()])
        .assert_request_mtls(true)
        .assert_request_body("token=foo&client_id=client")
        .set_response_body("{}")
        .set_response_status_code(200)
        .build();

    let (mut client, _) = get_clients();

    http_client.return_client_cert(true);

    client
        .introspect_async(&http_client, "foo".to_owned(), None, None)
        .await
        .unwrap();

    http_client.return_client_cert(false);

    let err = client
        .introspect_async(&http_client, "foo".to_owned(), None, None)
        .await
        .unwrap_err();

    assert!(err.is_type_error());
    assert_eq!(
        "mutual-TLS certificate and key not set",
        err.type_error().error.message
    );
}

#[tokio::test]
async fn requires_mtls_for_revocation_authentication_when_revocation_endpoint_auth_method_is_tls_client_auth(
) {
    let mut http_client = TestHttpReqRes::new("https://mtls.op.example.com/token/revoke")
        .assert_request_method(HttpMethod::POST)
        .assert_request_header(
            "content-type",
            vec!["application/x-www-form-urlencoded".to_string()],
        )
        .assert_request_header("content-length", vec!["26".to_string()])
        .assert_request_mtls(true)
        .assert_request_body("token=foo&client_id=client")
        .set_response_body("{}")
        .set_response_status_code(200)
        .build();

    let (mut client, _) = get_clients();

    http_client.return_client_cert(true);

    client
        .revoke_async(&http_client, "foo", None, None)
        .await
        .unwrap();

    http_client.return_client_cert(false);

    let err = client
        .revoke_async(&http_client, "foo", None, None)
        .await
        .unwrap_err();

    assert!(err.is_type_error());
    assert_eq!(
        "mutual-TLS certificate and key not set",
        err.type_error().error.message
    );
}