Repository: ramosbugs/openidconnect-rs Branch: main Commit: 202c8b1d9338 Files: 44 Total size: 712.2 KB Directory structure: gitextract_r84o10ci/ ├── .codecov.yml ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── UPGRADE.md ├── examples/ │ ├── gitlab.rs │ ├── google.rs │ └── okta_device_grant.rs ├── src/ │ ├── authorization.rs │ ├── claims.rs │ ├── client.rs │ ├── core/ │ │ ├── crypto.rs │ │ ├── jwk/ │ │ │ ├── mod.rs │ │ │ └── tests.rs │ │ ├── mod.rs │ │ └── tests.rs │ ├── discovery/ │ │ ├── mod.rs │ │ └── tests.rs │ ├── helpers.rs │ ├── http_utils.rs │ ├── id_token/ │ │ ├── mod.rs │ │ └── tests.rs │ ├── jwt/ │ │ ├── mod.rs │ │ └── tests.rs │ ├── lib.rs │ ├── logout.rs │ ├── macros.rs │ ├── registration/ │ │ ├── mod.rs │ │ └── tests.rs │ ├── token.rs │ ├── types/ │ │ ├── jwk.rs │ │ ├── jwks.rs │ │ ├── localized.rs │ │ ├── mod.rs │ │ └── tests.rs │ ├── user_info.rs │ └── verification/ │ ├── mod.rs │ └── tests.rs └── tests/ ├── rp_certification_code.rs ├── rp_certification_dynamic.rs └── rp_common.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .codecov.yml ================================================ ignore: - "tests/**" ================================================ FILE: .github/FUNDING.yml ================================================ github: [ramosbugs] ================================================ FILE: .github/workflows/main.yml ================================================ name: CI # Controls when the workflow will run on: # Triggers the workflow on push or pull request events but only for the main branch push: {} pull_request: {} schedule: # Run daily to catch breakages in new Rust versions as well as new cargo audit findings. - cron: '0 16 * * *' # Allows you to run this workflow manually from the Actions tab workflow_dispatch: env: CARGO_TERM_COLOR: always # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" test: # The type of runner that the job will run on runs-on: ${{ matrix.rust_os.os }} strategy: fail-fast: false matrix: rust_os: - { rust: 1.65.0, os: ubuntu-22.04 } - { rust: stable, os: ubuntu-22.04 } - { rust: beta, os: ubuntu-22.04 } - { rust: nightly, os: ubuntu-22.04 } env: CARGO_NET_GIT_FETCH_WITH_CLI: "true" # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - name: Print git branch name run: git rev-parse --abbrev-ref HEAD - run: git show-ref | grep $(git rev-parse HEAD) - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust_os.rust }} override: true components: clippy, rustfmt target: wasm32-unknown-unknown # Newer dependency versions may not support rustc 1.65, so we use a Cargo.lock file for those # builds. - name: Use Rust 1.65 lockfile if: ${{ matrix.rust_os.rust == '1.65.0' }} run: | cp Cargo-1.65.lock Cargo.lock echo "CARGO_LOCKED=--locked" >> $GITHUB_ENV - name: Run tests run: cargo ${CARGO_LOCKED} test --tests --examples - name: Doc tests run: | cargo ${CARGO_LOCKED} test --doc cargo ${CARGO_LOCKED} test --doc --no-default-features cargo ${CARGO_LOCKED} test --doc --all-features - name: Test with all features enabled run: cargo ${CARGO_LOCKED} test --all-features - name: Check fmt if: ${{ matrix.rust_os.rust == '1.65.0' }} run: cargo ${CARGO_LOCKED} fmt --all -- --check - name: Clippy if: ${{ matrix.rust_os.rust == '1.65.0' }} run: cargo ${CARGO_LOCKED} clippy --all --all-features -- --deny warnings - name: Audit if: ${{ matrix.rust_os.rust == 'stable' }} run: | cargo install --force cargo-audit # The chrono thread safety issue doesn't affect this crate since the crate does not rely # on the system's local time zone, only UTC. See: # https://github.com/chronotope/chrono/issues/499#issuecomment-946388161 # FIXME(ramosbugs/openidconnect-rs#140): upgrade `rsa` once fix for RUSTSEC-2023-0071 is # available. cargo ${CARGO_LOCKED} audit \ --ignore RUSTSEC-2020-0159 \ --ignore RUSTSEC-2023-0071 - name: Check WASM build run: cargo ${CARGO_LOCKED} check --target wasm32-unknown-unknown coverage: runs-on: ubuntu-latest container: image: xd009642/tarpaulin:0.32.0 options: --security-opt seccomp=unconfined steps: - uses: actions/checkout@v2 - name: Generate code coverage run: | cargo ${CARGO_LOCKED} tarpaulin --verbose --all-features --timeout 120 --out Xml - name: Upload to codecov.io uses: codecov/codecov-action@v3 with: fail_ci_if_error: false ================================================ FILE: .gitignore ================================================ /target/ **/*.rs.bk Cargo.lock *~ .DS_Store .idea/** *.iml ================================================ FILE: Cargo.toml ================================================ [package] name = "openidconnect" version = "4.0.1" authors = ["David A. Ramos "] description = "OpenID Connect library" keywords = ["openid", "oidc", "oauth2", "authentication", "auth"] license = "MIT" repository = "https://github.com/ramosbugs/openidconnect-rs" edition = "2021" readme = "README.md" rust-version = "1.65" [package.metadata.docs.rs] all-features = true [badges] maintenance = { status = "actively-developed" } [features] accept-rfc3339-timestamps = [] accept-string-booleans = [] curl = ["oauth2/curl"] default = ["reqwest", "rustls-tls"] native-tls = ["oauth2/native-tls"] reqwest = ["oauth2/reqwest"] reqwest-blocking = ["oauth2/reqwest-blocking"] rustls-tls = ["oauth2/rustls-tls"] timing-resistant-secret-traits = ["oauth2/timing-resistant-secret-traits"] ureq = ["oauth2/ureq"] [[example]] name = "gitlab" required-features = ["reqwest-blocking"] [[example]] name = "google" required-features = ["reqwest-blocking"] [[example]] name = "okta_device_grant" required-features = ["reqwest-blocking"] [dependencies] base64 = "0.22" # Disable 'time' dependency since it triggers RUSTSEC-2020-0071 and we don't need it. chrono = { version = "0.4", default-features = false, features = [ "clock", "std", "wasmbind" ] } thiserror = "1.0" http = "1.0" itertools = "0.14" log = "0.4" oauth2 = { version = "5.0.0", default-features = false } rand = "0.8.5" hmac = "0.12.1" rsa = "0.9.2" sha2 = { version = "0.10.6", features = ["oid"] } # Object ID needed for pkcs1v15 padding p256 = "0.13.2" p384 = "0.13.0" dyn-clone = "1.0.10" serde = "1.0" serde_json = "1.0" serde_path_to_error = "0.1" serde_plain = "1.0" serde_with = "3" serde-value = "0.7" url = { version = "2.4", features = ["serde"] } subtle = "2.4" ed25519-dalek = { version = "2.0.0", features = ["pem"] } [dev-dependencies] color-backtrace = { version = "0.5" } env_logger = "0.9" pretty_assertions = "1.0" reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false } retry = "1.0" anyhow = "1.0" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 David Ramos Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) Library for Rust [![crates.io](https://img.shields.io/crates/v/openidconnect.svg)](https://crates.io/crates/openidconnect) [![docs.rs](https://docs.rs/openidconnect/badge.svg)](https://docs.rs/openidconnect) [![Build Status](https://github.com/ramosbugs/openidconnect-rs/actions/workflows/main.yml/badge.svg)](https://github.com/ramosbugs/openidconnect-rs/actions/workflows/main.yml) [![codecov](https://codecov.io/gh/ramosbugs/openidconnect-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/ramosbugs/openidconnect-rs) This library provides extensible, strongly-typed interfaces for the OpenID Connect protocol, which can be used to authenticate users via [Google](https://developers.google.com/identity/openid-connect/openid-connect), [GitLab](https://docs.gitlab.com/ee/integration/openid_connect_provider.html), [Microsoft](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc), and [many other providers](https://openid.net/certification/#OPENID-OP-P). API documentation and examples are available on [docs.rs](https://docs.rs/openidconnect). ## Minimum Supported Rust Version (MSRV) The MSRV for *3.3* and newer releases of this crate is Rust **1.65**. The MSRV for *3.0* to *3.2* releases of this crate is Rust **1.57**. The MSRV for *2.x* releases of this crate is Rust 1.45. Since the 3.0.0 release, this crate maintains a policy of supporting Rust releases going back at least 6 months. Changes that break compatibility with Rust releases older than 6 months will no longer be considered SemVer breaking changes and will not result in a new major version number for this crate. MSRV changes will coincide with minor version updates and will not happen in patch releases. ## Standards * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) * Supported features: * Relying Party flows: code, implicit, hybrid * Standard claims * UserInfo endpoint * RSA, HMAC, ECDSA (P-256/P-384 curves) and EdDSA (Ed25519 curve) ID token verification * Unsupported features: * Aggregated and distributed claims * Passing request parameters as JWTs * Verification of the `azp` claim (see [discussion](https://bitbucket.org/openid/connect/issues/973/)) * ECDSA-based ID token verification using the P-521 curve * JSON Web Encryption (JWE) * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) * Supported features: * Provider Metadata * Unsupported features: * WebFinger * [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html) * Supported features: * Client Metadata * Client Registration endpoint * Unsupported features: * Client Configuration endpoint * [OpenID Connect RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) * [OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662) * [OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009) * [OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628) ================================================ FILE: UPGRADE.md ================================================ # Upgrade Guide ## Upgrading from 3.x to 4.x The 4.0 release includes breaking changes to address several long-standing API issues, along with a few minor improvements. Consider following the tips below to help ensure a smooth upgrade process. This document is not exhaustive but covers the breaking changes most likely to affect typical uses of this crate. ### Add typestate generic types to `Client` Each auth flow depends on one or more server endpoints. For example, the authorization code flow depends on both an authorization endpoint and a token endpoint, while the client credentials flow only depends on a token endpoint. Previously, it was possible to instantiate a `Client` without a token endpoint and then attempt to use an auth flow that required a token endpoint, leading to errors at runtime. Also, the authorization endpoint was always required, even for auth flows that do not use it. In the 4.0 release, all endpoints are optional. [Typestates](https://cliffle.com/blog/rust-typestate/) are used to statically track, at compile time, which endpoints' setters (e.g., `set_auth_uri()`) have been called. Auth flows that depend on an endpoint cannot be used without first calling the corresponding setter, which is enforced by the compiler's type checker. This guarantees that certain errors will not arise at runtime. When using [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) (i.e., `Client::from_provider_metadata()`), each discoverable endpoint is set to a conditional typestate (`EndpointMaybeSet`). This is because it cannot be determined at compile time whether each of these endpoints will be returned by the OpenID Provider. When the conditional typestate is set, endpoints can be used via fallible methods that return `Err(ConfigurationError::MissingUrl(_))` if an endpoint has not been set. There are three possible typestates, each implementing the `EndpointState` trait: * `EndpointNotSet`: the corresponding endpoint has **not** been set and cannot be used. * `EndpointSet`: the corresponding endpoint **has** been set and is ready to be used. * `EndpointMaybeSet`: the corresponding endpoint **may have** been set and can be used via fallible methods that return `Result<_, ConfigurationError>`. The following code changes are required to support the new interface: 1. Update calls to [`Client::new()`](https://docs.rs/openidconnect/latest/openidconnect/struct.Client.html#method.new) to use the three-argument constructor (which accepts only a `ClientId`, `IssuerUrl`, and `JsonWebKeySet`). Use the `set_auth_uri()`, `set_token_uri()`, `set_user_info_url()`, and `set_client_secret()` methods to set the authorization endpoint, token endpoint, user info endpoint, and client secret, respectively, if applicable to your application's auth flows. 2. If using `Client::from_provider_metadata()`, update call sites that use each auth flow (e.g., `Client::exchange_code()`) to handle the possibility of a `ConfigurationError` if the corresponding endpoint was not specified in the provider metadata. 3. If required by your usage of the `Client` or `CoreClient` types (i.e., if you see related compiler errors), add the following generic parameters: ```rust HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, ``` For example, if you store a `CoreClient` within another data type, you may need to annotate it as `CoreClient` if it has both an authorization endpoint and a token endpoint set. Compiler error messages will likely guide you to the appropriate combination of typestates. If, instead of using `CoreClient`, you are directly using `Client` with a different set of type parameters, you will need to append the five generic typestate parameters. For example, replace: ```rust type SpecialClient = Client< EmptyAdditionalClaims, CoreAuthDisplay, CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJsonWebKey, CoreAuthPrompt, StandardErrorResponse, SpecialTokenResponse, CoreTokenType, CoreTokenIntrospectionResponse, CoreRevocableToken, CoreRevocationErrorResponse, >; ``` with: ```rust type SpecialClient< HasAuthUrl = EndpointNotSet, HasDeviceAuthUrl = EndpointNotSet, HasIntrospectionUrl = EndpointNotSet, HasRevocationUrl = EndpointNotSet, HasTokenUrl = EndpointNotSet, HasUserInfoUrl = EndpointNotSet, > = Client< EmptyAdditionalClaims, CoreAuthDisplay, CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJsonWebKey, CoreAuthPrompt, StandardErrorResponse, SpecialTokenResponse, CoreTokenIntrospectionResponse, CoreRevocableToken, CoreRevocationErrorResponse, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, >; ``` The default values (`= EndpointNotSet`) are optional but often helpful since they will allow you to instantiate a client using `SpecialClient::new()` instead of having to specify `SpecialClient::::new()`. Also note that the `CoreJwsSigningAlgorithm` (`JS`), `CoreJsonWebKeyType` (`JT`), `CoreJsonWebKeyUse` (`JU`), and `CoreTokenType` (`TT`) type parameters have been removed (see below) since they are now implied by the `JsonWebKey` (`K`) and `TokenResponse` (`TR`)/`TokenIntrospectionResponse` (`TIR`) type parameters. ### Replace JWT-related generic traits with associated types Previously, the `JsonWebKey` trait had the following generic type parameters: ```rust JS: JwsSigningAlgorithm, JT: JsonWebKeyType, JU: JsonWebKeyUse, ``` In the 4.0 release, these generic type parameters have been removed and replaced with two associated types: ```rust /// Allowed key usage. type KeyUse: JsonWebKeyUse; /// JSON Web Signature (JWS) algorithm. type SigningAlgorithm: JwsSigningAlgorithm; ``` The `JT` type parameter was similarly removed from the `JwsSigningAlgorithm` trait and replaced with an associated type: ```rust /// Key type (e.g., RSA). type KeyType: JsonWebKeyType; ``` Similar changes were made to the lesser-used `PrivateSigningKey` and `JweContentEncryptionAlgorithm` traits. With the conversion to associated types, many generic type parameters throughout this crate became redundant and were removed in the 4.0 release. For example, the `Client` no longer needs the `JS`, `JT`, or `JU` parameters, which are implied by the `JsonWebKey` (`K`) type. ### Rename endpoint getters and setters for consistency The 2.0 release aimed to align the naming of each endpoint with the terminology used in the relevant RFC. For example, [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1) uses the term "endpoint URI" to refer to the authorization and token endpoints, while [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009#section-2) refers to the "token revocation endpoint URL," and [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2) uses neither "URI" nor "URL" to describe the introspection endpoint. However, the renaming in 2.0 was both internally inconsistent, and inconsistent with the specs. In 4.0, the `Client`'s getters and setters for each endpoint are now named as follows: * Authorization endpoint: `auth_uri()`/`set_auth_uri()` (newly added) * Token endpoint: `token_uri()`/`set_token_uri()` (newly added) * Redirect: `redirect_uri()`/`set_redirect_uri()` (no change to setter) * Revocation endpoint: `revocation_url()`/`set_revocation_url()` * Introspection endpoint: `introspection_url()`/`set_introspection_url()` * Device authorization endpoint: `device_authorization_url()`/`set_device_authorization_url()` * User info: `user_info_url()`/`set_user_info_url()` (newly added) ### Use stateful HTTP clients Previously, the HTTP clients provided by this crate were stateless. For example, the `openidconnect::reqwest::async_http_client()` method would instantiate a new `reqwest::Client` for each request. This meant that TCP connections could not be reused across requests, and customizing HTTP clients (e.g., adding a custom request header to every request) was inconvenient. The 4.0 release introduces two new traits: `AsyncHttpClient` and `SyncHttpClient`. Each `request_async()` and `request()` method now accepts a reference to a type that implements these traits, respectively, rather than a function type. > [!WARNING] > To prevent [SSRF](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) vulnerabilities, be sure to configure the HTTP client **not to follow redirects**. For example, use > [`redirect::Policy::none`](https://docs.rs/reqwest/latest/reqwest/redirect/struct.Policy.html#method.none) > when using `reqwest`, or > [`redirects(0)`](https://docs.rs/ureq/latest/ureq/struct.AgentBuilder.html#method.redirects) > when using `ureq`. The `AsyncHttpClient` trait is implemented for the following types: * `reqwest::Client` (when the default `reqwest` feature is enabled) * Any function type that implements: ```rust Fn(HttpRequest) -> F where E: std::error::Error + 'static, F: Future>, ``` To implement a custom asynchronous HTTP client, either directly implement the `AsyncHttpClient` trait, or use a function that implements the signature above. The `SyncHttpClient` trait is implemented for the following types: * `reqwest::blocking::Client` (when the `reqwest-blocking` feature is enabled; see below) * `ureq::Agent` (when the `ureq` feature is enabled) * `openidconnect::CurlHttpClient` (when the `curl` feature is enabled) * Any function type that implements: ```rust Fn(HttpRequest) -> Result where E: std::error::Error + 'static, ``` To implement a custom synchronous HTTP client, either directly implement the `SyncHttpClient` trait, or use a function that implements the signature above. ### Upgrade `http` to 1.0 and `reqwest` to 0.12 The 4.0 release of this crate depends on the new stable [`http`](https://docs.rs/http/latest/http/) 1.0 release, which affects various public interfaces. In particular, `reqwest` has been upgraded to 0.12, which uses `http` 1.0. ### Enable the `reqwest-blocking` feature to use the synchronous `reqwest` HTTP client In 4.0, enabling the (default) `reqwest` feature also enabled `reqwest`'s `blocking` feature. To reduce dependencies and improve compilation speed, the `reqwest` feature now only enables `reqwest`'s asynchronous (non-blocking) client. To use the synchronous (blocking) client, enable the `reqwest-blocking` feature in `Cargo.toml`: ```toml openidconnect = { version = "4", features = ["reqwest-blocking" ] } ``` ### Use `http::{Request, Response}` for custom HTTP clients The `HttpRequest` and `HttpResponse` structs have been replaced with type aliases to [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html) and [`http::Response`](https://docs.rs/http/latest/http/response/struct.Response.html), respectively. Custom HTTP clients will need to be updated to use the `http` types. See the [`reqwest` client implementations](https://github.com/ramosbugs/oauth2-rs/blob/23b952b23e6069525bc7e4c4f2c4924b8d28ce3a/src/reqwest.rs) in the underlying `oauth2` crate for an example. ### Replace `TT` generic type parameter in `OAuth2TokenResponse` with associated type Previously, the `TokenResponse`, `OAuth2TokenResponse`, and `TokenIntrospectionResponse` traits had a generic type parameter `TT: TokenType`. This has been replaced with an associated type called `TokenType` in `OAuth2TokenResponse` and `TokenIntrospectionResponse`. Uses of `CoreTokenResponse` and `CoreTokenIntrospectionResponse` should continue to work without changes, but custom implementations of either trait will need to be updated to replace the type parameter with an associated type. #### Remove `TT` generic type parameter from `Client` and each `*Request` type Removing the `TT` generic type parameter from `TokenResponse` (see above) made the `TT` parameters to `Client` and each `*Request` (e.g., `CodeTokenRequest`) redundant. Consequently, the `TT` parameter has been removed from each of these types. `CoreClient` should continue to work without any changes, but code that provides generic types for `Client` or any of the `*Response` types will need to be updated to remove the `TT` type parameter. ### Add `Display` to `ErrorResponse` trait To improve error messages, the [`RequestTokenError::ServerResponse`](https://docs.rs/oauth2/latest/oauth2/enum.RequestTokenError.html#variant.ServerResponse) enum variant now prints a message describing the server response using the `Display` trait. For most users (i.e., those using the default [`StandardErrorResponse`](https://docs.rs/oauth2/latest/oauth2/struct.StandardErrorResponse.html)), this does not require any code changes. However, users providing their own implementations of the `ErrorResponse` trait must now implement the `Display` trait. See `oauth2::StandardErrorResponse`'s [`Display` implementation](https://github.com/ramosbugs/oauth2-rs/blob/9d8f11addf819134f15c6d7f03276adb3d32e80b/src/error.rs#L88-L108) for an example. ### Remove the `jwk-alg` feature flag The 4.0 release removes the `jwk-alg` feature flag and unconditionally deserializes the optional `alg` field in `CoreJsonWebKey`. If a key specifies the `alg` field, the key may only be used for the purposes of verifying signatures using that specific JWS signature algorithm. By comparison, the 3.0 release ignored the `alg` field unless the `jwk-alg` feature flag was enabled. ### Enable the `timing-resistant-secret-traits` feature flag to securely compare secrets OpenID Connect flows require comparing secrets (e.g., `CsrfToken` and `Nonce`) received from providers. To do so securely while avoiding [timing side-channels](https://en.wikipedia.org/wiki/Timing_attack), the comparison must be done in constant time, either using a constant-time crate such as [`constant_time_eq`](https://crates.io/crates/constant_time_eq) (which could break if a future compiler version decides to be overly smart about its optimizations), or by first computing a cryptographically-secure hash (e.g., SHA-256) of both values and then comparing the hashes using `==`. The `timing-resistant-secret-traits` feature flag adds a safe (but comparatively expensive) `PartialEq` implementation to the secret types. Timing side-channels are why `PartialEq` is not auto-derived for this crate's secret types, and the lack of `PartialEq` is intended to prompt users to think more carefully about these comparisons. In the 3.0 release, the `Nonce` type implemented `PartialEq` by default, which also allowed the `IdToken`, `IdTokenClaims`, and `IdTokenFields` types to implement `PartialEq`. In 4.0, these types implement `PartialEq` only if the `timing-resistant-secret-traits` feature flag is enabled. ### Move `hash_bytes()` method from `JwsSignatureAlgorithm` trait to `JsonWebKey` Certain JWS signature algorithms (e.g., `EdDSA`) require information from the corresponding public key (e.g., the `crv` value) to determine which hash function to use for computing the `at_hash` and `c_hash` ID token claims. To accommodate this requirement, the 4.0 release moves the `hash_bytes()` method from the `JwsSignatureAlgorithm` trait to the `JsonWebKey` trait. The `AccessTokenHash::from_token()` and `AuthorizationCodeHash::from_code()` methods now require a `JsonWebKey` as an argument. ================================================ FILE: examples/gitlab.rs ================================================ //! //! This example showcases the process of integrating with the //! [GitLab OpenID Connect](https://docs.gitlab.com/ee/integration/openid_connect_provider.html) //! provider. //! //! Before running it, you'll need to generate your own //! [GitLab Application](https://docs.gitlab.com/ee/integration/oauth_provider.html). //! The application needs `openid`, `profile` and `email` permission. //! //! In order to run the example call: //! //! ```sh //! GITLAB_CLIENT_ID=xxx GITLAB_CLIENT_SECRET=yyy cargo run --example gitlab //! ``` //! //! ...and follow the instructions. //! use openidconnect::core::{ CoreClient, CoreGenderClaim, CoreIdTokenClaims, CoreIdTokenVerifier, CoreProviderMetadata, CoreResponseType, }; use openidconnect::reqwest; use openidconnect::{AdditionalClaims, UserInfoClaims}; use openidconnect::{ AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse, RedirectUrl, Scope, }; use serde::{Deserialize, Serialize}; use url::Url; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use std::process::exit; #[derive(Debug, Deserialize, Serialize)] struct GitLabClaims { // Deprecated and thus optional as it might be removed in the futre sub_legacy: Option, groups: Vec, } impl AdditionalClaims for GitLabClaims {} fn handle_error(fail: &T, msg: &'static str) { let mut err_msg = format!("ERROR: {}", msg); let mut cur_fail: Option<&dyn std::error::Error> = Some(fail); while let Some(cause) = cur_fail { err_msg += &format!("\n caused by: {}", cause); cur_fail = cause.source(); } println!("{}", err_msg); exit(1); } fn main() { env_logger::init(); let gitlab_client_id = ClientId::new( env::var("GITLAB_CLIENT_ID").expect("Missing the GITLAB_CLIENT_ID environment variable."), ); let gitlab_client_secret = ClientSecret::new( env::var("GITLAB_CLIENT_SECRET") .expect("Missing the GITLAB_CLIENT_SECRET environment variable."), ); let issuer_url = IssuerUrl::new("https://gitlab.com".to_string()).unwrap_or_else(|err| { handle_error(&err, "Invalid issuer URL"); unreachable!(); }); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .unwrap_or_else(|err| { handle_error(&err, "Failed to build HTTP client"); unreachable!(); }); // Fetch GitLab's OpenID Connect discovery document. let provider_metadata = CoreProviderMetadata::discover(&issuer_url, &http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed to discover OpenID Provider"); unreachable!(); }); // Set up the config for the GitLab OAuth2 process. let client = CoreClient::from_provider_metadata( provider_metadata, gitlab_client_id, Some(gitlab_client_secret), ) // This example will be running its own server at localhost:8080. // See below for the server implementation. .set_redirect_uri( RedirectUrl::new("http://localhost:8080".to_string()).unwrap_or_else(|err| { handle_error(&err, "Invalid redirect URL"); unreachable!(); }), ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state, nonce) = client .authorize_url( AuthenticationFlow::::AuthorizationCode, CsrfToken::new_random, Nonce::new_random, ) // This example is requesting access to the the user's profile including email. .add_scope(Scope::new("email".to_string())) .add_scope(Scope::new("profile".to_string())) .url(); println!("Open this URL in your browser:\n{authorize_url}\n"); let (code, state) = { // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); // Accept one connection let (mut stream, _) = listener.accept().unwrap(); let mut reader = BufReader::new(&stream); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); let redirect_url = request_line.split_whitespace().nth(1).unwrap(); let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); let code = url .query_pairs() .find(|(key, _)| key == "code") .map(|(_, code)| AuthorizationCode::new(code.into_owned())) .unwrap(); let state = url .query_pairs() .find(|(key, _)| key == "state") .map(|(_, state)| CsrfToken::new(state.into_owned())) .unwrap(); let message = "Go back to your terminal :)"; let response = format!( "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message ); stream.write_all(response.as_bytes()).unwrap(); (code, state) }; println!("GitLab returned the following code:\n{}\n", code.secret()); println!( "GitLab returned the following state:\n{} (expected `{}`)\n", state.secret(), csrf_state.secret() ); // Exchange the code with a token. let token_response = client .exchange_code(code) .unwrap_or_else(|err| { handle_error(&err, "No user info endpoint"); unreachable!(); }) .request(&http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed to contact token endpoint"); unreachable!(); }); println!( "GitLab returned access token:\n{}\n", token_response.access_token().secret() ); println!("GitLab returned scopes: {:?}", token_response.scopes()); let id_token_verifier: CoreIdTokenVerifier = client.id_token_verifier(); let id_token_claims: &CoreIdTokenClaims = token_response .extra_fields() .id_token() .expect("Server did not return an ID token") .claims(&id_token_verifier, &nonce) .unwrap_or_else(|err| { handle_error(&err, "Failed to verify ID token"); unreachable!(); }); println!("GitLab returned ID token: {:?}\n", id_token_claims); let userinfo_claims: UserInfoClaims = client .user_info(token_response.access_token().to_owned(), None) .unwrap_or_else(|err| { handle_error(&err, "No user info endpoint"); unreachable!(); }) .request(&http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed requesting user info"); unreachable!(); }); println!("GitLab returned UserInfo: {:?}", userinfo_claims); } ================================================ FILE: examples/google.rs ================================================ //! //! This example showcases the process of integrating with the //! [Google OpenID Connect](https://developers.google.com/identity/protocols/OpenIDConnect) //! provider. //! //! Before running it, you'll need to generate your own Google OAuth2 credentials. //! //! In order to run the example call: //! //! ```sh //! GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy cargo run --example google //! ``` //! //! ...and follow the instructions. //! use openidconnect::core::{ CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod, CoreGrantType, CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType, CoreRevocableToken, CoreSubjectIdentifierType, }; use openidconnect::reqwest; use openidconnect::{ AdditionalProviderMetadata, AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse, ProviderMetadata, RedirectUrl, RevocationUrl, Scope, }; use serde::{Deserialize, Serialize}; use url::Url; use std::env; use std::io::{BufRead, BufReader, Write}; use std::net::TcpListener; use std::process::exit; fn handle_error(fail: &T, msg: &'static str) { let mut err_msg = format!("ERROR: {}", msg); let mut cur_fail: Option<&dyn std::error::Error> = Some(fail); while let Some(cause) = cur_fail { err_msg += &format!("\n caused by: {}", cause); cur_fail = cause.source(); } println!("{}", err_msg); exit(1); } // Teach openidconnect-rs about a Google custom extension to the OpenID Discovery response that we can use as the RFC // 7009 OAuth 2.0 Token Revocation endpoint. For more information about the Google specific Discovery response see the // Google OpenID Connect service documentation at: https://developers.google.com/identity/protocols/oauth2/openid-connect#discovery #[derive(Clone, Debug, Deserialize, Serialize)] struct RevocationEndpointProviderMetadata { revocation_endpoint: String, } impl AdditionalProviderMetadata for RevocationEndpointProviderMetadata {} type GoogleProviderMetadata = ProviderMetadata< RevocationEndpointProviderMetadata, CoreAuthDisplay, CoreClientAuthMethod, CoreClaimName, CoreClaimType, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, >; fn main() { env_logger::init(); let google_client_id = ClientId::new( env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable."), ); let google_client_secret = ClientSecret::new( env::var("GOOGLE_CLIENT_SECRET") .expect("Missing the GOOGLE_CLIENT_SECRET environment variable."), ); let issuer_url = IssuerUrl::new("https://accounts.google.com".to_string()).unwrap_or_else(|err| { handle_error(&err, "Invalid issuer URL"); unreachable!(); }); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .unwrap_or_else(|err| { handle_error(&err, "Failed to build HTTP client"); unreachable!(); }); // Fetch Google's OpenID Connect discovery document. // // Note: If we don't care about token revocation we can simply use CoreProviderMetadata here // instead of GoogleProviderMetadata. If instead we wanted to optionally use the token // revocation endpoint if it seems to be supported we could do something like this: // #[derive(Clone, Debug, Deserialize, Serialize)] // struct AllOtherProviderMetadata(HashMap); // impl AdditionalClaims for AllOtherProviderMetadata {} // And then test for the presence of "revocation_endpoint" in the map returned by a call to // .additional_metadata(). let provider_metadata = GoogleProviderMetadata::discover(&issuer_url, &http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed to discover OpenID Provider"); unreachable!(); }); let revocation_endpoint = provider_metadata .additional_metadata() .revocation_endpoint .clone(); println!( "Discovered Google revocation endpoint: {}", revocation_endpoint ); // Set up the config for the Google OAuth2 process. let client = CoreClient::from_provider_metadata( provider_metadata, google_client_id, Some(google_client_secret), ) // This example will be running its own server at localhost:8080. // See below for the server implementation. .set_redirect_uri( RedirectUrl::new("http://localhost:8080".to_string()).unwrap_or_else(|err| { handle_error(&err, "Invalid redirect URL"); unreachable!(); }), ) // Google supports OAuth 2.0 Token Revocation (RFC-7009) .set_revocation_url( RevocationUrl::new(revocation_endpoint).unwrap_or_else(|err| { handle_error(&err, "Invalid revocation endpoint URL"); unreachable!(); }), ); // Generate the authorization URL to which we'll redirect the user. let (authorize_url, csrf_state, nonce) = client .authorize_url( AuthenticationFlow::::AuthorizationCode, CsrfToken::new_random, Nonce::new_random, ) // This example is requesting access to the "calendar" features and the user's profile. .add_scope(Scope::new("email".to_string())) .add_scope(Scope::new("profile".to_string())) .url(); println!("Open this URL in your browser:\n{}\n", authorize_url); let (code, state) = { // A very naive implementation of the redirect server. let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); // Accept one connection let (mut stream, _) = listener.accept().unwrap(); let mut reader = BufReader::new(&stream); let mut request_line = String::new(); reader.read_line(&mut request_line).unwrap(); let redirect_url = request_line.split_whitespace().nth(1).unwrap(); let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); let code = url .query_pairs() .find(|(key, _)| key == "code") .map(|(_, code)| AuthorizationCode::new(code.into_owned())) .unwrap(); let state = url .query_pairs() .find(|(key, _)| key == "state") .map(|(_, state)| CsrfToken::new(state.into_owned())) .unwrap(); let message = "Go back to your terminal :)"; let response = format!( "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", message.len(), message ); stream.write_all(response.as_bytes()).unwrap(); (code, state) }; println!("Google returned the following code:\n{}\n", code.secret()); println!( "Google returned the following state:\n{} (expected `{}`)\n", state.secret(), csrf_state.secret() ); // Exchange the code with a token. let token_response = client .exchange_code(code) .unwrap_or_else(|err| { handle_error(&err, "No user info endpoint"); unreachable!(); }) .request(&http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed to contact token endpoint"); unreachable!(); }); println!( "Google returned access token:\n{}\n", token_response.access_token().secret() ); println!("Google returned scopes: {:?}", token_response.scopes()); let id_token_verifier: CoreIdTokenVerifier = client.id_token_verifier(); let id_token_claims: &CoreIdTokenClaims = token_response .extra_fields() .id_token() .expect("Server did not return an ID token") .claims(&id_token_verifier, &nonce) .unwrap_or_else(|err| { handle_error(&err, "Failed to verify ID token"); unreachable!(); }); println!("Google returned ID token: {:?}", id_token_claims); // Revoke the obtained token let token_to_revoke: CoreRevocableToken = match token_response.refresh_token() { Some(token) => token.into(), None => token_response.access_token().into(), }; client .revoke_token(token_to_revoke) .unwrap_or_else(|err| { handle_error(&err, "Failed to revoke token"); unreachable!(); }) .request(&http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed to revoke token"); unreachable!(); }); } ================================================ FILE: examples/okta_device_grant.rs ================================================ //! //! This example showcases the process of using the device grant flow to obtain an ID token from the //! [Okta](https://developer.okta.com/docs/guides/device-authorization-grant/main/#request-the-device-verification-code) //! provider. //! //! Before running it, you'll need to generate your own //! [Okta Server](https://developer.okta.com/signup/). //! //! In order to run the example call: //! //! ```sh //! CLIENT_ID=xxx CLIENT_SECRET=yyy ISSUER_URL=zzz cargo run --example okta_device_grant //! ``` //! //! ...and follow the instructions. //! use openidconnect::core::{ CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod, CoreDeviceAuthorizationResponse, CoreGrantType, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, }; use openidconnect::reqwest; use openidconnect::{ AdditionalProviderMetadata, AuthType, ClientId, ClientSecret, DeviceAuthorizationUrl, IssuerUrl, ProviderMetadata, Scope, }; use serde::{Deserialize, Serialize}; use std::env; use std::process::exit; // Obtain the device_authorization_url from the OIDC metadata provider. #[derive(Clone, Debug, Deserialize, Serialize)] struct DeviceEndpointProviderMetadata { device_authorization_endpoint: DeviceAuthorizationUrl, } impl AdditionalProviderMetadata for DeviceEndpointProviderMetadata {} type DeviceProviderMetadata = ProviderMetadata< DeviceEndpointProviderMetadata, CoreAuthDisplay, CoreClientAuthMethod, CoreClaimName, CoreClaimType, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, >; fn handle_error(fail: &T, msg: &'static str) { let mut err_msg = format!("ERROR: {}", msg); let mut cur_fail: Option<&dyn std::error::Error> = Some(fail); while let Some(cause) = cur_fail { err_msg += &format!("\n caused by: {}", cause); cur_fail = cause.source(); } println!("{}", err_msg); exit(1); } fn main() -> Result<(), anyhow::Error> { env_logger::init(); let client_id = ClientId::new(env::var("CLIENT_ID").expect("Missing the CLIENT_ID environment variable.")); let client_secret = ClientSecret::new( env::var("CLIENT_SECRET").expect("Missing the CLIENT_SECRET environment variable."), ); let issuer_url = IssuerUrl::new( env::var("ISSUER_URL").expect("Missing the ISSUER_URL environment variable."), ) .unwrap_or_else(|err| { handle_error(&err, "Invalid issuer URL"); unreachable!(); }); let http_client = reqwest::blocking::ClientBuilder::new() // Following redirects opens the client up to SSRF vulnerabilities. .redirect(reqwest::redirect::Policy::none()) .build() .unwrap_or_else(|err| { handle_error(&err, "Failed to build HTTP client"); unreachable!(); }); // Fetch Okta's OpenID Connect discovery document. let provider_metadata = DeviceProviderMetadata::discover(&issuer_url, &http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed to discover OpenID Provider"); unreachable!(); }); // Use the custom metadata to get the device_authorization_endpoint let device_authorization_endpoint = provider_metadata .additional_metadata() .device_authorization_endpoint .clone(); // Set up the config for the Okta device authorization process. let client = CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) .set_device_authorization_url(device_authorization_endpoint) .set_auth_type(AuthType::RequestBody); let details: CoreDeviceAuthorizationResponse = client .exchange_device_code() .add_scope(Scope::new("profile".to_string())) .request(&http_client) .unwrap_or_else(|err| { handle_error(&err, "Failed to get device code"); unreachable!(); }); println!("Fetching device code..."); dbg!(&details); // Display the URL and user-code. println!( "Open this URL in your browser:\n{}\nand enter the code: {}", details.verification_uri_complete().unwrap().secret(), details.user_code().secret() ); // Now poll for the token let token = client .exchange_device_access_token(&details) .unwrap_or_else(|err| { handle_error(&err, "Failed to get access token"); unreachable!(); }) .request(&http_client, std::thread::sleep, None) .unwrap_or_else(|err| { handle_error(&err, "Failed to get access token"); unreachable!(); }); // Finally, display the ID Token to verify we are using OIDC println!("ID Token response: {:?}", token.extra_fields().id_token()); Ok(()) } ================================================ FILE: src/authorization.rs ================================================ use crate::core::CoreResponseType; use crate::helpers::join_vec; use crate::{ AdditionalClaims, AuthDisplay, AuthPrompt, AuthenticationContextClass, CsrfToken, GenderClaim, IdToken, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, LoginHint, Nonce, PkceCodeChallenge, RedirectUrl, ResponseType, Scope, }; use url::Url; use std::borrow::Cow; use std::time::Duration; /// Authentication flow, which determines how the Authorization Server returns the OpenID Connect /// ID token and OAuth2 access token to the Relying Party. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum AuthenticationFlow { /// Authorization Code Flow. /// /// The authorization server will return an OAuth2 authorization code. Clients must subsequently /// call `Client::exchange_code()` with the authorization code in order to retrieve an /// OpenID Connect ID token and OAuth2 access token. AuthorizationCode, /// Implicit Flow. /// /// Boolean value indicates whether an OAuth2 access token should also be returned. If `true`, /// the Authorization Server will return both an OAuth2 access token and OpenID Connect ID /// token. If `false`, it will return only an OpenID Connect ID token. Implicit(bool), /// Hybrid Flow. /// /// A hybrid flow according to [OAuth 2.0 Multiple Response Type Encoding Practices]( /// https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html). The enum value /// contains the desired `response_type`s. See /// [Section 3](https://openid.net/specs/openid-connect-core-1_0.html#Authentication) for /// details. Hybrid(Vec), } /// A request to the authorization endpoint. pub struct AuthorizationRequest<'a, AD, P, RT> where AD: AuthDisplay, P: AuthPrompt, RT: ResponseType, { pub(crate) inner: oauth2::AuthorizationRequest<'a>, pub(crate) acr_values: Vec, pub(crate) authentication_flow: AuthenticationFlow, pub(crate) claims_locales: Vec, pub(crate) display: Option, pub(crate) id_token_hint: Option, pub(crate) login_hint: Option, pub(crate) max_age: Option, pub(crate) nonce: Nonce, pub(crate) prompts: Vec

, pub(crate) ui_locales: Vec, } impl<'a, AD, P, RT> AuthorizationRequest<'a, AD, P, RT> where AD: AuthDisplay, P: AuthPrompt, RT: ResponseType, { /// Appends a new scope to the authorization URL. pub fn add_scope(mut self, scope: Scope) -> Self { self.inner = self.inner.add_scope(scope); self } /// Appends a collection of scopes to the authorization URL. pub fn add_scopes(mut self, scopes: I) -> Self where I: IntoIterator, { self.inner = self.inner.add_scopes(scopes); self } /// Appends an extra param to the authorization URL. /// /// This method allows extensions to be used without direct support from /// this crate. If `name` conflicts with a parameter managed by this crate, the /// behavior is undefined. In particular, do not set parameters defined by /// [RFC 6749](https://tools.ietf.org/html/rfc6749) or /// [RFC 7636](https://tools.ietf.org/html/rfc7636). /// /// # Security Warning /// /// Callers should follow the security recommendations for any OAuth2 extensions used with /// this function, which are beyond the scope of /// [RFC 6749](https://tools.ietf.org/html/rfc6749). pub fn add_extra_param(mut self, name: N, value: V) -> Self where N: Into>, V: Into>, { self.inner = self.inner.add_extra_param(name, value); self } /// Enables the use of [Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636) /// (PKCE). /// /// PKCE is *highly recommended* for all public clients (i.e., those for which there /// is no client secret or for which the client secret is distributed with the client, /// such as in a native, mobile app, or browser app). pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self { self.inner = self.inner.set_pkce_challenge(pkce_code_challenge); self } /// Requests Authentication Context Class Reference values. /// /// ACR values should be added in order of preference. The Authentication Context Class /// satisfied by the authentication performed is accessible from the ID token via the /// [`IdTokenClaims::auth_context_ref()`](crate::IdTokenClaims::auth_context_ref) method. pub fn add_auth_context_value(mut self, acr_value: AuthenticationContextClass) -> Self { self.acr_values.push(acr_value); self } /// Requests the preferred languages for claims returned by the OpenID Connect Provider. /// /// Languages should be added in order of preference. pub fn add_claims_locale(mut self, claims_locale: LanguageTag) -> Self { self.claims_locales.push(claims_locale); self } // TODO: support 'claims' parameter // https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter /// Specifies how the OpenID Connect Provider displays the authentication and consent user /// interfaces to the end user. pub fn set_display(mut self, display: AD) -> Self { self.display = Some(display); self } /// Provides an ID token previously issued by this OpenID Connect Provider as a hint about /// the user's identity. /// /// This field should be set whenever /// [`CoreAuthPrompt::None`](crate::core::CoreAuthPrompt::None) is used (see /// [`AuthorizationRequest::add_prompt`]), it but may be provided for any authorization /// request. pub fn set_id_token_hint( mut self, id_token_hint: &'a IdToken, ) -> Self where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { self.id_token_hint = Some(id_token_hint.to_string()); self } /// Provides the OpenID Connect Provider with a hint about the user's identity. /// /// The nature of this hint is specific to each provider. pub fn set_login_hint(mut self, login_hint: LoginHint) -> Self { self.login_hint = Some(login_hint); self } /// Sets a maximum amount of time since the user has last authenticated with the OpenID /// Connect Provider. /// /// If more time has elapsed, the provider forces the user to re-authenticate. pub fn set_max_age(mut self, max_age: Duration) -> Self { self.max_age = Some(max_age); self } /// Specifies what level of authentication and consent prompts the OpenID Connect Provider /// should present to the user. pub fn add_prompt(mut self, prompt: P) -> Self { self.prompts.push(prompt); self } /// Requests the preferred languages for the user interface presented by the OpenID Connect /// Provider. /// /// Languages should be added in order of preference. pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self { self.ui_locales.push(ui_locale); self } /// Overrides the `redirect_url` to the one specified. pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) -> Self { self.inner = self.inner.set_redirect_uri(redirect_url); self } /// Returns the full authorization URL and CSRF state for this authorization /// request. pub fn url(self) -> (Url, CsrfToken, Nonce) { let response_type = match self.authentication_flow { AuthenticationFlow::AuthorizationCode => CoreResponseType::Code.to_oauth2(), AuthenticationFlow::Implicit(include_token) => { if include_token { oauth2::ResponseType::new( [CoreResponseType::IdToken, CoreResponseType::Token] .iter() .map(|response_type| response_type.as_ref()) .collect::>() .join(" "), ) } else { CoreResponseType::IdToken.to_oauth2() } } AuthenticationFlow::Hybrid(ref response_types) => oauth2::ResponseType::new( response_types .iter() .map(|response_type| response_type.as_ref()) .collect::>() .join(" "), ), }; let (mut inner, nonce) = ( self.inner .set_response_type(&response_type) .add_extra_param("nonce", self.nonce.secret().clone()), self.nonce, ); if !self.acr_values.is_empty() { inner = inner.add_extra_param("acr_values", join_vec(&self.acr_values)); } if !self.claims_locales.is_empty() { inner = inner.add_extra_param("claims_locales", join_vec(&self.claims_locales)); } if let Some(ref display) = self.display { inner = inner.add_extra_param("display", display.as_ref()); } if let Some(ref id_token_hint) = self.id_token_hint { inner = inner.add_extra_param("id_token_hint", id_token_hint); } if let Some(ref login_hint) = self.login_hint { inner = inner.add_extra_param("login_hint", login_hint.secret()); } if let Some(max_age) = self.max_age { inner = inner.add_extra_param("max_age", max_age.as_secs().to_string()); } if !self.prompts.is_empty() { inner = inner.add_extra_param("prompt", join_vec(&self.prompts)); } if !self.ui_locales.is_empty() { inner = inner.add_extra_param("ui_locales", join_vec(&self.ui_locales)); } let (url, state) = inner.url(); (url, state, nonce) } } #[cfg(test)] mod tests { use crate::core::CoreAuthenticationFlow; use crate::core::{CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreIdToken, CoreResponseType}; use crate::IssuerUrl; use crate::{ AuthUrl, AuthenticationContextClass, AuthenticationFlow, ClientId, ClientSecret, CsrfToken, EndpointNotSet, EndpointSet, JsonWebKeySet, LanguageTag, LoginHint, Nonce, RedirectUrl, Scope, TokenUrl, }; use std::borrow::Cow; use std::time::Duration; fn new_client() -> CoreClient< EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointSet, EndpointNotSet, > { color_backtrace::install(); CoreClient::new( ClientId::new("aaa".to_string()), IssuerUrl::new("https://example".to_string()).unwrap(), JsonWebKeySet::default(), ) .set_client_secret(ClientSecret::new("bbb".to_string())) .set_auth_uri(AuthUrl::new("https://example/authorize".to_string()).unwrap()) .set_token_uri(TokenUrl::new("https://example/token".to_string()).unwrap()) } #[test] fn test_authorize_url_minimal() { let client = new_client(); let (authorize_url, _, _) = client .authorize_url( AuthenticationFlow::AuthorizationCode::, || CsrfToken::new("CSRF123".to_string()), || Nonce::new("NONCE456".to_string()), ) .url(); assert_eq!( "https://example/authorize?response_type=code&client_id=aaa&\ state=CSRF123&scope=openid&nonce=NONCE456", authorize_url.to_string() ); } #[test] fn test_authorize_url_implicit_with_access_token() { let client = new_client(); let (authorize_url, _, _) = client .authorize_url( AuthenticationFlow::::Implicit(true), || CsrfToken::new("CSRF123".to_string()), || Nonce::new("NONCE456".to_string()), ) .url(); assert_eq!( "https://example/authorize?response_type=id_token+token&client_id=aaa&\ state=CSRF123&scope=openid&nonce=NONCE456", authorize_url.to_string() ); } #[test] fn test_authorize_url_hybrid() { let client = new_client(); let (authorize_url, _, _) = client .authorize_url( AuthenticationFlow::Hybrid(vec![ CoreResponseType::Code, CoreResponseType::Extension("other".to_string()), ]), || CsrfToken::new("CSRF123".to_string()), || Nonce::new("NONCE456".to_string()), ) .url(); assert_eq!( "https://example/authorize?response_type=code+other&client_id=aaa&\ state=CSRF123&scope=openid&nonce=NONCE456", authorize_url.to_string() ); } #[test] fn test_authorize_url_full() { let client = new_client() .set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap()); let flow = CoreAuthenticationFlow::AuthorizationCode; fn new_csrf() -> CsrfToken { CsrfToken::new("CSRF123".to_string()) } fn new_nonce() -> Nonce { Nonce::new("NONCE456".to_string()) } let (authorize_url, _, _) = client .authorize_url(flow.clone(), new_csrf, new_nonce) .add_scope(Scope::new("email".to_string())) .set_display(CoreAuthDisplay::Touch) .add_prompt(CoreAuthPrompt::Login) .add_prompt(CoreAuthPrompt::Consent) .set_max_age(Duration::from_secs(1800)) .add_ui_locale(LanguageTag::new("fr-CA".to_string())) .add_ui_locale(LanguageTag::new("fr".to_string())) .add_ui_locale(LanguageTag::new("en".to_string())) .add_auth_context_value(AuthenticationContextClass::new( "urn:mace:incommon:iap:silver".to_string(), )) .url(); assert_eq!( "https://example/authorize?response_type=code&client_id=aaa&\ state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&\ nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", authorize_url.to_string() ); let serialized_jwt = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjpbIm15X2NsaWVudCJdL\ CJleHAiOjE1NDQ5MzIxNDksImlhdCI6MTU0NDkyODU0OSwiYXV0aF90aW1lIjoxNTQ0OTI4NTQ4LCJub25jZSI\ 6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IiLCJzdWIiOiJzdWJqZWN0In0.gb5HuuyDMu-LvYvG-jJNIJPEZ\ 823qNwvgNjdAtW0HJpgwJWhJq0hOHUuZz6lvf8ud5xbg5GOo0Q37v3Ke08TvGu6E1USWjecZzp1aYVm9BiMvw5\ EBRUrwAaOCG2XFjuOKUVfglSMJnRnoNqVVIWpCAr1ETjZzRIbkU3n5GQRguC5CwN5n45I3dtjoKuNGc2Ni-IMl\ J2nRiCJOl2FtStdgs-doc-A9DHtO01x-5HCwytXvcE28Snur1JnqpUgmWrQ8gZMGuijKirgNnze2Dd5BsZRHZ2\ CLGIwBsCnauBrJy_NNlQg4hUcSlGsuTa0dmZY7mCf4BN2WCpyOh0wgtkAgQ"; let id_token = serde_json::from_value::(serde_json::Value::String( serialized_jwt.to_string(), )) .unwrap(); let (authorize_url, _, _) = client .authorize_url(flow.clone(), new_csrf, new_nonce) .add_scope(Scope::new("email".to_string())) .set_display(CoreAuthDisplay::Touch) .set_id_token_hint(&id_token) .set_login_hint(LoginHint::new("foo@bar.com".to_string())) .add_prompt(CoreAuthPrompt::Login) .add_prompt(CoreAuthPrompt::Consent) .set_max_age(Duration::from_secs(1800)) .add_ui_locale(LanguageTag::new("fr-CA".to_string())) .add_ui_locale(LanguageTag::new("fr".to_string())) .add_ui_locale(LanguageTag::new("en".to_string())) .add_auth_context_value(AuthenticationContextClass::new( "urn:mace:incommon:iap:silver".to_string(), )) .add_extra_param("foo", "bar") .url(); assert_eq!( format!( "https://example/authorize?response_type=code&client_id=aaa&state=CSRF123&\ redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&foo=bar&\ nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ id_token_hint={}&login_hint=foo%40bar.com&\ max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", serialized_jwt ), authorize_url.to_string() ); let (authorize_url, _, _) = client .authorize_url(flow, new_csrf, new_nonce) .add_scopes(vec![ Scope::new("email".to_string()), Scope::new("profile".to_string()), ]) .set_display(CoreAuthDisplay::Touch) .set_id_token_hint(&id_token) .set_login_hint(LoginHint::new("foo@bar.com".to_string())) .add_prompt(CoreAuthPrompt::Login) .add_prompt(CoreAuthPrompt::Consent) .set_max_age(Duration::from_secs(1800)) .add_ui_locale(LanguageTag::new("fr-CA".to_string())) .add_ui_locale(LanguageTag::new("fr".to_string())) .add_ui_locale(LanguageTag::new("en".to_string())) .add_auth_context_value(AuthenticationContextClass::new( "urn:mace:incommon:iap:silver".to_string(), )) .add_extra_param("foo", "bar") .url(); assert_eq!( format!( "https://example/authorize?response_type=code&client_id=aaa&state=CSRF123&\ redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email+profile&foo=bar&\ nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ id_token_hint={}&login_hint=foo%40bar.com&\ max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", serialized_jwt ), authorize_url.to_string() ); } #[test] fn test_authorize_url_redirect_url_override() { let client = new_client() .set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap()); let flow = CoreAuthenticationFlow::AuthorizationCode; fn new_csrf() -> CsrfToken { CsrfToken::new("CSRF123".to_string()) } fn new_nonce() -> Nonce { Nonce::new("NONCE456".to_string()) } let (authorize_url, _, _) = client .authorize_url(flow, new_csrf, new_nonce) .add_scope(Scope::new("email".to_string())) .set_display(CoreAuthDisplay::Touch) .add_prompt(CoreAuthPrompt::Login) .add_prompt(CoreAuthPrompt::Consent) .set_max_age(Duration::from_secs(1800)) .add_ui_locale(LanguageTag::new("fr-CA".to_string())) .add_ui_locale(LanguageTag::new("fr".to_string())) .add_ui_locale(LanguageTag::new("en".to_string())) .add_auth_context_value(AuthenticationContextClass::new( "urn:mace:incommon:iap:silver".to_string(), )) .set_redirect_uri(Cow::Owned( RedirectUrl::new("http://localhost:8888/alternative".to_string()).unwrap(), )) .url(); assert_eq!( "https://example/authorize?response_type=code&client_id=aaa&\ state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Falternative&scope=openid+email&\ nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\ max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en", authorize_url.to_string() ); } } ================================================ FILE: src/claims.rs ================================================ use crate::helpers::{Boolean, DeserializeMapField, FlattenFilter, Timestamp}; use crate::types::localized::split_language_tag_key; use crate::{ AddressCountry, AddressLocality, AddressPostalCode, AddressRegion, EndUserBirthday, EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, EndUserWebsiteUrl, FormattedAddress, LanguageTag, LocalizedClaim, StreetAddress, SubjectIdentifier, }; use chrono::{DateTime, Utc}; use serde::de::{DeserializeOwned, Deserializer, MapAccess, Visitor}; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize, Serializer}; use serde_with::skip_serializing_none; use std::fmt::{Debug, Formatter, Result as FormatterResult}; use std::marker::PhantomData; use std::str; /// Additional claims beyond the set of Standard Claims defined by OpenID Connect Core. pub trait AdditionalClaims: Debug + DeserializeOwned + Serialize + 'static {} /// No additional claims. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] // In order to support serde flatten, this must be an empty struct rather than an empty // tuple struct. pub struct EmptyAdditionalClaims {} impl AdditionalClaims for EmptyAdditionalClaims {} /// Address claims. #[skip_serializing_none] #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct AddressClaim { /// Full mailing address, formatted for display or use on a mailing label. /// /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented /// either as a carriage return/line feed pair (`\r\n`) or as a single line feed character /// (`\n`). pub formatted: Option, /// Full street address component, which MAY include house number, street name, Post Office Box, /// and multi-line extended street address information. /// /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented /// either as a carriage return/line feed pair (`\r\n`) or as a single line feed character /// (`\n`). pub street_address: Option, /// City or locality component. pub locality: Option, /// State, province, prefecture, or region component. pub region: Option, /// Zip code or postal code component. pub postal_code: Option, /// Country name component. pub country: Option, } /// Gender claim. pub trait GenderClaim: Clone + Debug + DeserializeOwned + Serialize + 'static {} /// Standard Claims defined by OpenID Connect Core. #[derive(Clone, Debug, PartialEq, Eq)] pub struct StandardClaims where GC: GenderClaim, { pub(crate) sub: SubjectIdentifier, pub(crate) name: Option>, pub(crate) given_name: Option>, pub(crate) family_name: Option>, pub(crate) middle_name: Option>, pub(crate) nickname: Option>, pub(crate) preferred_username: Option, pub(crate) profile: Option>, pub(crate) picture: Option>, pub(crate) website: Option>, pub(crate) email: Option, pub(crate) email_verified: Option, pub(crate) gender: Option, pub(crate) birthday: Option, pub(crate) birthdate: Option, pub(crate) zoneinfo: Option, pub(crate) locale: Option, pub(crate) phone_number: Option, pub(crate) phone_number_verified: Option, pub(crate) address: Option, pub(crate) updated_at: Option>, } impl StandardClaims where GC: GenderClaim, { /// Initializes a set of Standard Claims. /// /// The Subject (`sub`) claim is the only required Standard Claim. pub fn new(subject: SubjectIdentifier) -> Self { Self { sub: subject, name: None, given_name: None, family_name: None, middle_name: None, nickname: None, preferred_username: None, profile: None, picture: None, website: None, email: None, email_verified: None, gender: None, birthday: None, birthdate: None, zoneinfo: None, locale: None, phone_number: None, phone_number_verified: None, address: None, updated_at: None, } } /// Returns the Subject (`sub`) claim. pub fn subject(&self) -> &SubjectIdentifier { &self.sub } /// Sets the Subject (`sub`) claim. pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self { self.sub = subject; self } field_getters_setters![ pub self [self] ["claim"] { set_name -> name[Option>], set_given_name -> given_name[Option>], set_family_name -> family_name[Option>], set_middle_name -> middle_name[Option>], set_nickname -> nickname[Option>], set_preferred_username -> preferred_username[Option], set_profile -> profile[Option>], set_picture -> picture[Option>], set_website -> website[Option>], set_email -> email[Option], set_email_verified -> email_verified[Option], set_gender -> gender[Option], set_birthday -> birthday[Option], set_birthdate -> birthdate[Option], set_zoneinfo -> zoneinfo[Option], set_locale -> locale[Option], set_phone_number -> phone_number[Option], set_phone_number_verified -> phone_number_verified[Option], set_address -> address[Option], set_updated_at -> updated_at[Option>], } ]; } impl FlattenFilter for StandardClaims where GC: GenderClaim, { // When another struct (i.e., additional claims) is co-flattened with this one, only include // fields in that other struct which are not part of this struct. fn should_include(field_name: &str) -> bool { !matches!( split_language_tag_key(field_name), ("sub", None) | ("name", _) | ("given_name", _) | ("family_name", _) | ("middle_name", _) | ("nickname", _) | ("preferred_username", None) | ("profile", _) | ("picture", _) | ("website", _) | ("email", None) | ("email_verified", None) | ("gender", None) | ("birthday", None) | ("birthdate", None) | ("zoneinfo", None) | ("locale", None) | ("phone_number", None) | ("phone_number_verified", None) | ("address", None) | ("updated_at", None) ) } } impl<'de, GC> Deserialize<'de> for StandardClaims where GC: GenderClaim, { /// Special deserializer that supports [RFC 5646](https://tools.ietf.org/html/rfc5646) language /// tags associated with human-readable client metadata fields. fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct ClaimsVisitor(PhantomData); impl<'de, GC> Visitor<'de> for ClaimsVisitor where GC: GenderClaim, { type Value = StandardClaims; fn expecting(&self, formatter: &mut Formatter) -> FormatterResult { formatter.write_str("struct StandardClaims") } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'de>, { // NB: The non-localized fields are actually Option> here so that we can // distinguish between omitted fields and fields explicitly set to `null`. The // latter is necessary so that we can detect duplicate fields (e.g., if a key is // present both with a null value and a non-null value, that's an error). let mut sub = None; let mut name = None; let mut given_name = None; let mut family_name = None; let mut middle_name = None; let mut nickname = None; let mut preferred_username = None; let mut profile = None; let mut picture = None; let mut website = None; let mut email = None; let mut email_verified = None; let mut gender = None; let mut birthday = None; let mut birthdate = None; let mut zoneinfo = None; let mut locale = None; let mut phone_number = None; let mut phone_number_verified = None; let mut address = None; let mut updated_at = None; macro_rules! field_case { ($field:ident, $typ:ty, $language_tag:ident) => {{ $field = Some(<$typ>::deserialize_map_field( &mut map, stringify!($field), $language_tag, $field, )?); }}; } while let Some(key) = map.next_key::()? { let (field_name, language_tag) = split_language_tag_key(&key); match field_name { "sub" => field_case!(sub, SubjectIdentifier, language_tag), "name" => field_case!(name, LocalizedClaim>, language_tag), "given_name" => { field_case!(given_name, LocalizedClaim>, language_tag) } "family_name" => { field_case!(family_name, LocalizedClaim>, language_tag) } "middle_name" => { field_case!(middle_name, LocalizedClaim>, language_tag) } "nickname" => { field_case!(nickname, LocalizedClaim>, language_tag) } "preferred_username" => { field_case!(preferred_username, Option<_>, language_tag) } "profile" => { field_case!(profile, LocalizedClaim>, language_tag) } "picture" => { field_case!(picture, LocalizedClaim>, language_tag) } "website" => { field_case!(website, LocalizedClaim>, language_tag) } "email" => field_case!(email, Option<_>, language_tag), "email_verified" => { field_case!(email_verified, Option, language_tag) } "gender" => field_case!(gender, Option<_>, language_tag), "birthday" => field_case!(birthday, Option<_>, language_tag), "birthdate" => field_case!(birthdate, Option<_>, language_tag), "zoneinfo" => field_case!(zoneinfo, Option<_>, language_tag), "locale" => field_case!(locale, Option<_>, language_tag), "phone_number" => field_case!(phone_number, Option<_>, language_tag), "phone_number_verified" => { field_case!(phone_number_verified, Option, language_tag) } "address" => field_case!(address, Option<_>, language_tag), "updated_at" => field_case!(updated_at, Option, language_tag), // Ignore unknown fields. _ => { map.next_value::()?; continue; } }; } Ok(StandardClaims { sub: sub.ok_or_else(|| serde::de::Error::missing_field("sub"))?, name: name.and_then(LocalizedClaim::flatten_or_none), given_name: given_name.and_then(LocalizedClaim::flatten_or_none), family_name: family_name.and_then(LocalizedClaim::flatten_or_none), middle_name: middle_name.and_then(LocalizedClaim::flatten_or_none), nickname: nickname.and_then(LocalizedClaim::flatten_or_none), preferred_username: preferred_username.flatten(), profile: profile.and_then(LocalizedClaim::flatten_or_none), picture: picture.and_then(LocalizedClaim::flatten_or_none), website: website.and_then(LocalizedClaim::flatten_or_none), email: email.flatten(), email_verified: email_verified.flatten().map(Boolean::into_inner), gender: gender.flatten(), birthday: birthday.flatten(), birthdate: birthdate.flatten(), zoneinfo: zoneinfo.flatten(), locale: locale.flatten(), phone_number: phone_number.flatten(), phone_number_verified: phone_number_verified.flatten().map(Boolean::into_inner), address: address.flatten(), updated_at: updated_at .flatten() .map(|sec| { sec.to_utc().map_err(|_| { serde::de::Error::custom(format!( "failed to parse `{sec}` as UTC datetime (in seconds) for key \ `updated_at`" )) }) }) .transpose()?, }) } } deserializer.deserialize_map(ClaimsVisitor(PhantomData)) } } impl Serialize for StandardClaims where GC: GenderClaim, { #[allow(clippy::cognitive_complexity)] fn serialize(&self, serializer: SE) -> Result where SE: Serializer, { serialize_fields! { self -> serializer { [sub] [LanguageTag(name)] [LanguageTag(given_name)] [LanguageTag(family_name)] [LanguageTag(middle_name)] [LanguageTag(nickname)] [Option(preferred_username)] [LanguageTag(profile)] [LanguageTag(picture)] [LanguageTag(website)] [Option(email)] [Option(email_verified)] [Option(gender)] [Option(birthday)] [Option(birthdate)] [Option(zoneinfo)] [Option(locale)] [Option(phone_number)] [Option(phone_number_verified)] [Option(address)] [Option(DateTime(Seconds(updated_at)))] } } } } #[cfg(test)] mod tests { use crate::core::CoreGenderClaim; use crate::StandardClaims; // The spec states (https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse): // "If a Claim is not returned, that Claim Name SHOULD be omitted from the JSON object // representing the Claims; it SHOULD NOT be present with a null or empty string value." // However, we still aim to support identity providers that disregard this suggestion. #[test] fn test_null_optional_claims() { let claims = serde_json::from_str::>( r#"{ "sub": "24400320", "name": null, "given_name": null, "family_name": null, "middle_name": null, "nickname": null, "preferred_username": null, "profile": null, "picture": null, "website": null, "email": null, "email_verified": null, "gender": null, "birthday": null, "birthdate": null, "zoneinfo": null, "locale": null, "phone_number": null, "phone_number_verified": null, "address": null, "updated_at": null }"#, ) .expect("should deserialize successfully"); assert_eq!(claims.subject().as_str(), "24400320"); assert_eq!(claims.name(), None); } fn expect_err_prefix( result: Result, serde_json::Error>, expected_prefix: &str, ) { let err_str = result.expect_err("deserialization should fail").to_string(); assert!( err_str.starts_with(expected_prefix), "error message should begin with `{}`: {}", expected_prefix, err_str, ) } #[test] fn test_duplicate_claims() { expect_err_prefix( serde_json::from_str( r#"{ "sub": "24400320", "sub": "24400321" }"#, ), "duplicate field `sub` at line", ); expect_err_prefix( serde_json::from_str( r#"{ "name": null, "sub": "24400320", "name": "foo", }"#, ), "duplicate field `name` at line", ); expect_err_prefix( serde_json::from_str( r#"{ "name#en": null, "sub": "24400320", "name#en": "foo", }"#, ), "duplicate field `name#en` at line", ); } #[test] fn test_err_field_name() { expect_err_prefix( serde_json::from_str( r#"{ "sub": 24400320 }"#, ), "sub: invalid type: integer `24400320`, expected a string at line", ); } } ================================================ FILE: src/client.rs ================================================ use crate::{ AccessToken, AdditionalClaims, AdditionalProviderMetadata, AuthDisplay, AuthPrompt, AuthType, AuthUrl, AuthenticationFlow, AuthorizationCode, AuthorizationRequest, ClaimName, ClaimType, ClientAuthMethod, ClientCredentialsTokenRequest, ClientId, ClientSecret, CodeTokenRequest, ConfigurationError, CsrfToken, DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationResponse, DeviceAuthorizationUrl, EndpointMaybeSet, EndpointNotSet, EndpointSet, EndpointState, ErrorResponse, ExtraDeviceAuthorizationFields, GenderClaim, GrantType, IdTokenVerifier, IntrospectionRequest, IntrospectionUrl, IssuerUrl, JsonWebKey, JsonWebKeySet, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, Nonce, PasswordTokenRequest, ProviderMetadata, RedirectUrl, RefreshToken, RefreshTokenRequest, ResourceOwnerPassword, ResourceOwnerUsername, ResponseMode, ResponseType, RevocableToken, RevocationRequest, RevocationUrl, Scope, SubjectIdentifier, SubjectIdentifierType, TokenIntrospectionResponse, TokenResponse, TokenUrl, UserInfoRequest, UserInfoUrl, }; use std::marker::PhantomData; const OPENID_SCOPE: &str = "openid"; /// OpenID Connect client. /// /// # Error Types /// /// To enable compile time verification that only the correct and complete set of errors for the `Client` function being /// invoked are exposed to the caller, the `Client` type is specialized on multiple implementations of the /// [`ErrorResponse`] trait. The exact [`ErrorResponse`] implementation returned varies by the RFC that the invoked /// `Client` function implements: /// /// - Generic type `TE` (aka Token Error) for errors defined by [RFC 6749 OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749). /// - Generic type `TRE` (aka Token Revocation Error) for errors defined by [RFC 7009 OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009). /// /// For example when revoking a token, error code `unsupported_token_type` (from RFC 7009) may be returned: /// ```rust /// # use http::status::StatusCode; /// # use http::header::{HeaderValue, CONTENT_TYPE}; /// # use openidconnect::core::CoreClient; /// # use openidconnect::{ /// # AccessToken, /// # AuthUrl, /// # ClientId, /// # ClientSecret, /// # HttpResponse, /// # IssuerUrl, /// # JsonWebKeySet, /// # RequestTokenError, /// # RevocationErrorResponseType, /// # RevocationUrl, /// # TokenUrl, /// # }; /// # use thiserror::Error; /// # /// # let client = /// # CoreClient::new( /// # ClientId::new("aaa".to_string()), /// # IssuerUrl::new("https://example".to_string()).unwrap(), /// # JsonWebKeySet::default(), /// # ) /// # .set_client_secret(ClientSecret::new("bbb".to_string())) /// # .set_auth_uri(AuthUrl::new("https://example/authorize".to_string()).unwrap()) /// # .set_token_uri(TokenUrl::new("https://example/token".to_string()).unwrap()) /// # .set_revocation_url(RevocationUrl::new("https://revocation/url".to_string()).unwrap()); /// # /// # #[derive(Debug, Error)] /// # enum FakeError { /// # #[error("error")] /// # Err, /// # } /// # /// # let http_client = |_| -> Result { /// # Ok(http::Response::builder() /// # .status(StatusCode::BAD_REQUEST) /// # .header(CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap()) /// # .body( /// # r#"{"error": "unsupported_token_type", /// # "error_description": "stuff happened", /// # "error_uri": "https://errors"}"# /// # .to_string() /// # .into_bytes(), /// # ) /// # .unwrap()) /// # }; /// # /// let res = client /// .revoke_token(AccessToken::new("some token".to_string()).into()) /// .unwrap() /// .request(&http_client); /// /// assert!(matches!(res, Err( /// RequestTokenError::ServerResponse(err)) if matches!(err.error(), /// RevocationErrorResponseType::UnsupportedTokenType))); /// ``` #[derive(Clone, Debug)] pub struct Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, { oauth2_client: oauth2::Client< TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, >, pub(crate) client_id: ClientId, client_secret: Option, pub(crate) issuer: IssuerUrl, userinfo_endpoint: Option, pub(crate) jwks: JsonWebKeySet, id_token_signing_algs: Option>, use_openid_scope: bool, _phantom: PhantomData<(AC, AD, GC, JE, P, HasUserInfoUrl)>, } impl Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, { /// Initialize an OpenID Connect client. pub fn new(client_id: ClientId, issuer: IssuerUrl, jwks: JsonWebKeySet) -> Self { Client { oauth2_client: oauth2::Client::new(client_id.clone()), client_id, client_secret: None, issuer, userinfo_endpoint: None, jwks, id_token_signing_algs: None, use_openid_scope: true, _phantom: PhantomData, } } } impl Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointMaybeSet, EndpointMaybeSet, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, { /// Initialize an OpenID Connect client from OpenID Connect Discovery provider metadata. /// /// Use [`ProviderMetadata::discover`] or /// [`ProviderMetadata::discover_async`] to fetch the provider metadata. pub fn from_provider_metadata( provider_metadata: ProviderMetadata, client_id: ClientId, client_secret: Option, ) -> Self where A: AdditionalProviderMetadata, CA: ClientAuthMethod, CN: ClaimName, CT: ClaimType, G: GrantType, JK: JweKeyManagementAlgorithm, RM: ResponseMode, RS: ResponseType, S: SubjectIdentifierType, { let mut oauth2_client = oauth2::Client::new(client_id.clone()) .set_auth_uri(provider_metadata.authorization_endpoint().clone()) .set_token_uri_option(provider_metadata.token_endpoint().cloned()); if let Some(ref client_secret) = client_secret { oauth2_client = oauth2_client.set_client_secret(client_secret.to_owned()); } Client { oauth2_client, client_id, client_secret, issuer: provider_metadata.issuer().clone(), userinfo_endpoint: provider_metadata.userinfo_endpoint().cloned(), jwks: provider_metadata.jwks().to_owned(), id_token_signing_algs: Some( provider_metadata .id_token_signing_alg_values_supported() .to_owned(), ), use_openid_scope: true, _phantom: PhantomData, } } } impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, { /// Set the type of client authentication used for communicating with the authorization /// server. /// /// The default is to use HTTP Basic authentication, as recommended in /// [Section 2.3.1 of RFC 6749](https://tools.ietf.org/html/rfc6749#section-2.3.1). Note that /// if a client secret is omitted (i.e., [`set_client_secret()`](Self::set_client_secret) is not /// called), [`AuthType::RequestBody`] is used regardless of the `auth_type` passed to /// this function. pub fn set_auth_type(mut self, auth_type: AuthType) -> Self { self.oauth2_client = self.oauth2_client.set_auth_type(auth_type); self } /// Return the type of client authentication used for communicating with the authorization /// server. pub fn auth_type(&self) -> &AuthType { self.oauth2_client.auth_type() } /// Set the authorization endpoint. /// /// The client uses the authorization endpoint to obtain authorization from the resource owner /// via user-agent redirection. This URL is used in all standard OAuth2 flows except the /// [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) /// and the [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4). pub fn set_auth_uri( self, auth_uri: AuthUrl, ) -> Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, EndpointSet, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > { Client { oauth2_client: self.oauth2_client.set_auth_uri(auth_uri), client_id: self.client_id, client_secret: self.client_secret, issuer: self.issuer, userinfo_endpoint: self.userinfo_endpoint, jwks: self.jwks, id_token_signing_algs: self.id_token_signing_algs, use_openid_scope: self.use_openid_scope, _phantom: PhantomData, } } /// Return the Client ID. pub fn client_id(&self) -> &ClientId { &self.client_id } /// Set the client secret. /// /// A client secret is generally used for confidential (i.e., server-side) OAuth2 clients and /// omitted from public (browser or native app) OAuth2 clients (see /// [RFC 8252](https://tools.ietf.org/html/rfc8252)). pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self { self.oauth2_client = self.oauth2_client.set_client_secret(client_secret.clone()); self.client_secret = Some(client_secret); self } /// Set the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint used /// for the Device Authorization Flow. /// /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn set_device_authorization_url( self, device_authorization_url: DeviceAuthorizationUrl, ) -> Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, EndpointSet, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > { Client { oauth2_client: self .oauth2_client .set_device_authorization_url(device_authorization_url), client_id: self.client_id, client_secret: self.client_secret, issuer: self.issuer, userinfo_endpoint: self.userinfo_endpoint, jwks: self.jwks, id_token_signing_algs: self.id_token_signing_algs, use_openid_scope: self.use_openid_scope, _phantom: PhantomData, } } /// Set the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. /// /// See [`introspect()`](Self::introspect). pub fn set_introspection_url( self, introspection_url: IntrospectionUrl, ) -> Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, EndpointSet, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > { Client { oauth2_client: self.oauth2_client.set_introspection_url(introspection_url), client_id: self.client_id, client_secret: self.client_secret, issuer: self.issuer, userinfo_endpoint: self.userinfo_endpoint, jwks: self.jwks, id_token_signing_algs: self.id_token_signing_algs, use_openid_scope: self.use_openid_scope, _phantom: PhantomData, } } /// Set the redirect URL used by the authorization endpoint. pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self { self.oauth2_client = self.oauth2_client.set_redirect_uri(redirect_url); self } /// Return the redirect URL used by the authorization endpoint. pub fn redirect_uri(&self) -> Option<&RedirectUrl> { self.oauth2_client.redirect_uri() } /// Set the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. /// /// See [`revoke_token()`](Self::revoke_token). pub fn set_revocation_url( self, revocation_uri: RevocationUrl, ) -> Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, EndpointSet, HasTokenUrl, HasUserInfoUrl, > { Client { oauth2_client: self.oauth2_client.set_revocation_url(revocation_uri), client_id: self.client_id, client_secret: self.client_secret, issuer: self.issuer, userinfo_endpoint: self.userinfo_endpoint, jwks: self.jwks, id_token_signing_algs: self.id_token_signing_algs, use_openid_scope: self.use_openid_scope, _phantom: PhantomData, } } /// Set the token endpoint. /// /// The client uses the token endpoint to exchange an authorization code for an access token, /// typically with client authentication. This URL is used in /// all standard OAuth2 flows except the /// [Implicit Grant](https://tools.ietf.org/html/rfc6749#section-4.2). pub fn set_token_uri( self, token_uri: TokenUrl, ) -> Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, EndpointSet, HasUserInfoUrl, > { Client { oauth2_client: self.oauth2_client.set_token_uri(token_uri), client_id: self.client_id, client_secret: self.client_secret, issuer: self.issuer, userinfo_endpoint: self.userinfo_endpoint, jwks: self.jwks, id_token_signing_algs: self.id_token_signing_algs, use_openid_scope: self.use_openid_scope, _phantom: PhantomData, } } /// Set the user info endpoint. /// /// See [`user_info()`](Self::user_info). pub fn set_user_info_url( self, userinfo_endpoint: UserInfoUrl, ) -> Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, EndpointSet, > { Client { oauth2_client: self.oauth2_client, client_id: self.client_id, client_secret: self.client_secret, issuer: self.issuer, userinfo_endpoint: Some(userinfo_endpoint), jwks: self.jwks, id_token_signing_algs: self.id_token_signing_algs, use_openid_scope: self.use_openid_scope, _phantom: PhantomData, } } /// Enable the `openid` scope to be requested automatically. /// /// This scope is requested by default, so this function is only useful after previous calls to /// [`disable_openid_scope`][Client::disable_openid_scope]. pub fn enable_openid_scope(mut self) -> Self { self.use_openid_scope = true; self } /// Disable the `openid` scope from being requested automatically. pub fn disable_openid_scope(mut self) -> Self { self.use_openid_scope = false; self } /// Return an ID token verifier for use with the [`IdToken::claims`](crate::IdToken::claims) /// method. pub fn id_token_verifier(&self) -> IdTokenVerifier { let verifier = if let Some(ref client_secret) = self.client_secret { IdTokenVerifier::new_confidential_client( self.client_id.clone(), client_secret.clone(), self.issuer.clone(), self.jwks.clone(), ) } else { IdTokenVerifier::new_public_client( self.client_id.clone(), self.issuer.clone(), self.jwks.clone(), ) }; if let Some(id_token_signing_algs) = self.id_token_signing_algs.clone() { verifier.set_allowed_algs(id_token_signing_algs) } else { verifier } } } /// Methods requiring an authorization endpoint. impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, EndpointSet, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, { /// Return the authorization endpoint. pub fn auth_uri(&self) -> &AuthUrl { self.oauth2_client.auth_uri() } /// Generate an authorization URL for a new authorization request. /// /// Requires [`set_auth_uri()`](Self::set_auth_uri) to have been previously /// called to set the authorization endpoint. /// /// NOTE: [Passing authorization request parameters as a JSON Web Token /// ](https://openid.net/specs/openid-connect-core-1_0.html#JWTRequests) /// instead of URL query parameters is not currently supported. The /// [`claims` parameter](https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter) /// is also not directly supported, although the [`AuthorizationRequest::add_extra_param`] /// method can be used to add custom parameters, including `claims`. /// /// # Arguments /// /// * `authentication_flow` - The authentication flow to use (code, implicit, or hybrid). /// * `state_fn` - A function that returns an opaque value used by the client to maintain state /// between the request and callback. The authorization server includes this value when /// redirecting the user-agent back to the client. /// * `nonce_fn` - Similar to `state_fn`, but used to generate an opaque nonce to be used /// when verifying the ID token returned by the OpenID Connect Provider. /// /// # Security Warning /// /// Callers should use a fresh, unpredictable `state` for each authorization request and verify /// that this value matches the `state` parameter passed by the authorization server to the /// redirect URI. Doing so mitigates /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) /// attacks. /// /// Similarly, callers should use a fresh, unpredictable `nonce` to help protect against ID /// token reuse and forgery. pub fn authorize_url( &self, authentication_flow: AuthenticationFlow, state_fn: SF, nonce_fn: NF, ) -> AuthorizationRequest where NF: FnOnce() -> Nonce + 'static, RS: ResponseType, SF: FnOnce() -> CsrfToken + 'static, { let request = AuthorizationRequest { inner: self.oauth2_client.authorize_url(state_fn), acr_values: Vec::new(), authentication_flow, claims_locales: Vec::new(), display: None, id_token_hint: None, login_hint: None, max_age: None, nonce: nonce_fn(), prompts: Vec::new(), ui_locales: Vec::new(), }; if self.use_openid_scope { request.add_scope(Scope::new(OPENID_SCOPE.to_string())) } else { request } } } /// Methods requiring a token endpoint. impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, EndpointSet, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasUserInfoUrl: EndpointState, { /// Request an access token using the /// [Client Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). /// /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously /// called to set the token endpoint. pub fn exchange_client_credentials(&self) -> ClientCredentialsTokenRequest { self.oauth2_client.exchange_client_credentials() } /// Exchange a code returned during the /// [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) /// for an access token. /// /// Acquires ownership of the `code` because authorization codes may only be used once to /// retrieve an access token from the authorization server. /// /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously /// called to set the token endpoint. pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest { self.oauth2_client.exchange_code(code) } /// Exchange an [RFC 8628](https://tools.ietf.org/html/rfc8628#section-3.2) Device Authorization /// Response returned by [`exchange_device_code()`](Self::exchange_device_code) for an access /// token. /// /// Requires [`set_token_uri()`](Self::set_token_uri) to have been previously /// called to set the token endpoint. pub fn exchange_device_access_token<'a, EF>( &'a self, auth_response: &'a DeviceAuthorizationResponse, ) -> DeviceAccessTokenRequest<'a, 'static, TR, EF> where EF: ExtraDeviceAuthorizationFields, { self.oauth2_client .exchange_device_access_token(auth_response) } /// Request an access token using the /// [Resource Owner Password Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3). /// /// Requires /// [`set_token_uri()`](Self::set_token_uri) to have /// been previously called to set the token endpoint. pub fn exchange_password<'a>( &'a self, username: &'a ResourceOwnerUsername, password: &'a ResourceOwnerPassword, ) -> PasswordTokenRequest<'a, TE, TR> { self.oauth2_client.exchange_password(username, password) } /// Exchange a refresh token for an access token. /// /// See . /// /// Requires /// [`set_token_uri()`](Self::set_token_uri) to have /// been previously called to set the token endpoint. pub fn exchange_refresh_token<'a>( &'a self, refresh_token: &'a RefreshToken, ) -> RefreshTokenRequest<'a, TE, TR> { self.oauth2_client.exchange_refresh_token(refresh_token) } /// Return the token endpoint. pub fn token_uri(&self) -> &TokenUrl { self.oauth2_client.token_uri() } } /// Methods with a possibly-set token endpoint after calling /// [`from_provider_metadata()`](Self::from_provider_metadata). impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, EndpointMaybeSet, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasUserInfoUrl: EndpointState, { /// Request an access token using the /// [Client Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). /// /// Requires [`from_provider_metadata()`](Self::from_provider_metadata) to have been previously /// called to construct the client. pub fn exchange_client_credentials( &self, ) -> Result, ConfigurationError> { self.oauth2_client.exchange_client_credentials() } /// Exchange a code returned during the /// [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1) /// for an access token. /// /// Acquires ownership of the `code` because authorization codes may only be used once to /// retrieve an access token from the authorization server. /// /// Requires [`from_provider_metadata()`](Self::from_provider_metadata) to have been previously /// called to construct the client. pub fn exchange_code( &self, code: AuthorizationCode, ) -> Result, ConfigurationError> { self.oauth2_client.exchange_code(code) } /// Exchange an [RFC 8628](https://tools.ietf.org/html/rfc8628#section-3.2) Device Authorization /// Response returned by [`exchange_device_code()`](Self::exchange_device_code) for an access /// token. /// /// Requires [`from_provider_metadata()`](Self::from_provider_metadata) to have been previously /// called to construct the client. pub fn exchange_device_access_token<'a, EF>( &'a self, auth_response: &'a DeviceAuthorizationResponse, ) -> Result, ConfigurationError> where EF: ExtraDeviceAuthorizationFields, { self.oauth2_client .exchange_device_access_token(auth_response) } /// Request an access token using the /// [Resource Owner Password Credentials Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3). /// /// Requires [`from_provider_metadata()`](Self::from_provider_metadata) to have been previously /// called to construct the client. pub fn exchange_password<'a>( &'a self, username: &'a ResourceOwnerUsername, password: &'a ResourceOwnerPassword, ) -> Result, ConfigurationError> { self.oauth2_client.exchange_password(username, password) } /// Exchange a refresh token for an access token. /// /// See . /// /// Requires [`from_provider_metadata()`](Self::from_provider_metadata) to have been previously /// called to construct the client. pub fn exchange_refresh_token<'a>( &'a self, refresh_token: &'a RefreshToken, ) -> Result, ConfigurationError> { self.oauth2_client.exchange_refresh_token(refresh_token) } /// Return the token endpoint. pub fn token_uri(&self) -> Option<&TokenUrl> { self.oauth2_client.token_uri() } } /// Methods requiring a device authorization endpoint. impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, EndpointSet, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, { /// Begin the [RFC 8628](https://tools.ietf.org/html/rfc8628) Device Authorization Flow and /// retrieve a Device Authorization Response. /// /// Requires /// [`set_device_authorization_url()`](Self::set_device_authorization_url) to have /// been previously called to set the device authorization endpoint. /// /// See [`exchange_device_access_token()`](Self::exchange_device_access_token). pub fn exchange_device_code(&self) -> DeviceAuthorizationRequest { let request = self.oauth2_client.exchange_device_code(); if self.use_openid_scope { request.add_scope(Scope::new(OPENID_SCOPE.to_string())) } else { request } } /// Return the [RFC 8628](https://tools.ietf.org/html/rfc8628) device authorization endpoint /// used for the Device Authorization Flow. /// /// See [`exchange_device_code()`](Self::exchange_device_code). pub fn device_authorization_url(&self) -> &DeviceAuthorizationUrl { self.oauth2_client.device_authorization_url() } } /// Methods requiring an introspection endpoint. impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, EndpointSet, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, { /// Retrieve metadata for an access token using the /// [`RFC 7662`](https://tools.ietf.org/html/rfc7662) introspection endpoint. /// /// Requires [`set_introspection_url()`](Self::set_introspection_url) to have been previously /// called to set the introspection endpoint. pub fn introspect<'a>(&'a self, token: &'a AccessToken) -> IntrospectionRequest<'a, TE, TIR> { self.oauth2_client.introspect(token) } /// Return the [RFC 7662](https://tools.ietf.org/html/rfc7662) introspection endpoint. pub fn introspection_url(&self) -> &IntrospectionUrl { self.oauth2_client.introspection_url() } } /// Methods requiring a revocation endpoint. impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasTokenUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, EndpointSet, HasTokenUrl, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, { /// Revoke an access or refresh token using the [RFC 7009](https://tools.ietf.org/html/rfc7009) /// revocation endpoint. /// /// Requires [`set_revocation_url()`](Self::set_revocation_url) to have been previously /// called to set the revocation endpoint. pub fn revoke_token( &self, token: RT, ) -> Result, ConfigurationError> { self.oauth2_client.revoke_token(token) } /// Return the [RFC 7009](https://tools.ietf.org/html/rfc7009) revocation endpoint. /// /// See [`revoke_token()`](Self::revoke_token()). pub fn revocation_url(&self) -> &RevocationUrl { self.oauth2_client.revocation_url() } } /// Methods requiring a user info endpoint. impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, EndpointSet, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Request info about the user associated with the given access token. /// /// Requires [`set_user_info_url()`](Self::set_user_info_url) to have been previously /// called to set the user info endpoint. /// /// To help protect against token substitution attacks, this function optionally allows clients /// to provide the subject identifier whose user info they expect to receive. If provided and /// the subject returned by the OpenID Connect Provider does not match, the /// [`UserInfoRequest::request`] or [`UserInfoRequest::request_async`] functions will return /// [`UserInfoError::ClaimsVerification`](crate::UserInfoError::ClaimsVerification). If set to /// `None`, any subject is accepted. pub fn user_info( &self, access_token: AccessToken, expected_subject: Option, ) -> UserInfoRequest { self.user_info_impl(self.user_info_url(), access_token, expected_subject) } /// Return the user info endpoint. /// /// See ['user_info()'](Self::user_info). pub fn user_info_url(&self) -> &UserInfoUrl { // This is enforced statically via the HasUserInfo generic type. self.userinfo_endpoint .as_ref() .expect("should have user info endpoint") } } /// Methods with a possibly-set user info endpoint. impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, EndpointMaybeSet, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, { /// Request info about the user associated with the given access token. /// /// Requires [`from_provider_metadata()`](Self::from_provider_metadata) to have been previously /// called to construct the client. /// /// To help protect against token substitution attacks, this function optionally allows clients /// to provide the subject identifier whose user info they expect to receive. If provided and /// the subject returned by the OpenID Connect Provider does not match, the /// [`UserInfoRequest::request`] or [`UserInfoRequest::request_async`] functions will return /// [`UserInfoError::ClaimsVerification`](crate::UserInfoError::ClaimsVerification). If set to /// `None`, any subject is accepted. pub fn user_info( &self, access_token: AccessToken, expected_subject: Option, ) -> Result, ConfigurationError> { Ok(self.user_info_impl( self.userinfo_endpoint .as_ref() .ok_or(ConfigurationError::MissingUrl("user info"))?, access_token, expected_subject, )) } /// Return the user info endpoint. /// /// See ['user_info()'](Self::user_info). pub fn user_info_url(&self) -> Option<&UserInfoUrl> { self.userinfo_endpoint.as_ref() } } ================================================ FILE: src/core/crypto.rs ================================================ use crate::core::jwk::CoreJsonCurveType; use crate::core::{CoreJsonWebKey, CoreJsonWebKeyType}; use crate::helpers::Base64UrlEncodedBytes; use crate::{JsonWebKey, SignatureVerificationError}; use std::ops::Deref; fn rsa_public_key( key: &CoreJsonWebKey, ) -> Result<(&Base64UrlEncodedBytes, &Base64UrlEncodedBytes), String> { if *key.key_type() != CoreJsonWebKeyType::RSA { Err("RSA key required".to_string()) } else { let n = key .n .as_ref() .ok_or_else(|| "RSA modulus `n` is missing".to_string())?; let e = key .e .as_ref() .ok_or_else(|| "RSA exponent `e` is missing".to_string())?; Ok((n, e)) } } fn ec_public_key( key: &CoreJsonWebKey, ) -> Result< ( &Base64UrlEncodedBytes, &Base64UrlEncodedBytes, &CoreJsonCurveType, ), String, > { if *key.key_type() != CoreJsonWebKeyType::EllipticCurve { Err("EC key required".to_string()) } else { let x = key .x .as_ref() .ok_or_else(|| "EC `x` part is missing".to_string())?; let y = key .y .as_ref() .ok_or_else(|| "EC `y` part is missing".to_string())?; let crv = key .crv .as_ref() .ok_or_else(|| "EC `crv` part is missing".to_string())?; Ok((x, y, crv)) } } fn ed_public_key( key: &CoreJsonWebKey, ) -> Result<(&Base64UrlEncodedBytes, &CoreJsonCurveType), String> { if *key.key_type() != CoreJsonWebKeyType::OctetKeyPair { Err("OKP key required".to_string()) } else { let x = key .x .as_ref() .ok_or_else(|| "OKP `x` part is missing".to_string())?; let crv = key .crv .as_ref() .ok_or_else(|| "OKP `crv` part is missing".to_string())?; Ok((x, crv)) } } pub fn verify_rsa_signature( key: &CoreJsonWebKey, padding: impl rsa::traits::SignatureScheme, msg: &[u8], signature: &[u8], ) -> Result<(), SignatureVerificationError> { let (n, e) = rsa_public_key(key).map_err(SignatureVerificationError::InvalidKey)?; // let's n and e as a big integers to prevent issues with leading zeros // according to https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1.1 // `n` is always unsigned (hence has sign plus) let n_bigint = rsa::BigUint::from_bytes_be(n.deref()); let e_bigint = rsa::BigUint::from_bytes_be(e.deref()); let public_key = rsa::RsaPublicKey::new(n_bigint, e_bigint) .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; public_key .verify(padding, msg, signature) .map_err(|_| SignatureVerificationError::CryptoError("bad signature".to_string())) } /// According to RFC5480, Section-2.2 implementations of Elliptic Curve Cryptography MUST support the uncompressed form. /// The first octet of the octet string indicates whether the uncompressed or compressed form is used. For the uncompressed /// form, the first octet has to be 0x04. /// According to https://briansmith.org/rustdoc/ring/signature/index.html#ecdsa__fixed-details-fixed-length-pkcs11-style-ecdsa-signatures, /// to recover the X and Y coordinates from an octet string, the Octet-String-To-Elliptic-Curve-Point Conversion /// is used (Section 2.3.4 of https://www.secg.org/sec1-v2.pdf). pub fn verify_ec_signature( key: &CoreJsonWebKey, msg: &[u8], signature: &[u8], ) -> Result<(), SignatureVerificationError> { use p256::ecdsa::signature::Verifier; let (x, y, crv) = ec_public_key(key).map_err(SignatureVerificationError::InvalidKey)?; let mut pk = vec![0x04]; pk.extend(x.deref()); pk.extend(y.deref()); match *crv { CoreJsonCurveType::P256 => { let public_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&pk) .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; public_key .verify( msg, &p256::ecdsa::Signature::from_slice(signature).map_err(|_| { SignatureVerificationError::CryptoError("Invalid signature".to_string()) })?, ) .map_err(|_| { SignatureVerificationError::CryptoError("EC Signature was wrong".to_string()) }) } CoreJsonCurveType::P384 => { let public_key = p384::ecdsa::VerifyingKey::from_sec1_bytes(&pk) .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; public_key .verify( msg, &p384::ecdsa::Signature::from_slice(signature).map_err(|_| { SignatureVerificationError::CryptoError("Invalid signature".to_string()) })?, ) .map_err(|_| { SignatureVerificationError::CryptoError("EC Signature was wrong".to_string()) }) } CoreJsonCurveType::P521 => Err(SignatureVerificationError::UnsupportedAlg( "P521".to_string(), )), _ => Err(SignatureVerificationError::InvalidKey(format!( "unrecognized curve `{crv:?}`" ))), } } pub fn verify_ed_signature( key: &CoreJsonWebKey, msg: &[u8], signature: &[u8], ) -> Result<(), SignatureVerificationError> { use ed25519_dalek::Verifier; let (x, crv) = ed_public_key(key).map_err(SignatureVerificationError::InvalidKey)?; match *crv { CoreJsonCurveType::Ed25519 => { let public_key = ed25519_dalek::VerifyingKey::try_from(x.deref().as_slice()) .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; public_key .verify( msg, &ed25519_dalek::Signature::from_slice(signature).map_err(|_| { SignatureVerificationError::CryptoError("invalid signature".to_string()) })?, ) .map_err(|_| { SignatureVerificationError::CryptoError("incorrect EdDSA signature".to_string()) }) } _ => Err(SignatureVerificationError::InvalidKey(format!( "unrecognized curve `{crv:?}`" ))), } } #[cfg(test)] mod tests { use crate::core::crypto::verify_rsa_signature; use crate::core::CoreJsonWebKey; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use sha2::Digest; #[test] fn test_leading_zeros_are_parsed_correctly() { // The message we signed let msg = "THIS IS A SIGNATURE TEST"; let signature = BASE64_URL_SAFE_NO_PAD.decode("bg0ohqKwYHAiODeG6qkJ-6IhodN7LGPxAh4hbWeIoBdSXrXMt8Ft8U0BV7vANPvF56h20XB9C0021x2kt7iAbMgPNcZ7LCuXMPPq04DrBpMHafH5BXBwnyDKJKrzDm5sfr6OgEkcxSLHaSJ6gTWQ3waPt6_SeH2-Fi74rg13MHyX-0iqz7bZveoBbGIs5yQCwvXgrDS9zW5LUwUHozHfE6FuSi_Z92ioXeu7FHHDg1KFfg3hs8ZLx4wAX15Vw2GCQOzvyNdbItxXRLnrN1NPqxFquVNo5RGlx6ihR1Jfe7y_n0NSR2q2TuU4cIwR0LRwEaANy5SDqtleQPrTEn8nGQ").unwrap(); // RSA pub key with leading 0 let key : CoreJsonWebKey = serde_json::from_value(serde_json::json!( { "kty": "RSA", "e": "AQAB", "use": "sig", "kid": "TEST_KEY_ID", "alg": "RS256", "n": "AN0M6Y760b9Ok2PxDOps1TgSmiOaR9mLIfUHtZ_o-6JypOckGcl1CxrteyokOb3WyDsfIAN9fFNrycv5YoLKO7sh0IcfzNEXFgzK84HTBcGuqhN8NV98Z6N9EryUrgJYsJeVoPYm0MzkDe4NyWHhnq-9OyNCQzVELH0NhhViQqRyM92OPrJcQlk8s3ZvcgRmkd-rEtRua8SbS3GEvfvgweVy5-qcJCGoziKfx-IteMOm6yKoHvqisKb91N-qw_kSS4YQUx-DZVDo2g24F7VIbcYzJGUOU674HUF1j-wJyXzG3VV8lAXD8hABs5Lh87gr8_hIZD5gbYBJRObJk9XZbfk" } )).unwrap(); let mut hasher = sha2::Sha256::new(); hasher.update(msg); let hash = hasher.finalize().to_vec(); assert! { verify_rsa_signature( &key, rsa::Pkcs1v15Sign::new::(), &hash, &signature, ).is_ok() } } } ================================================ FILE: src/core/jwk/mod.rs ================================================ use crate::core::{crypto, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm}; use crate::helpers::{deserialize_option_or_none, Base64UrlEncodedBytes}; use crate::types::jwks::check_key_compatibility; use crate::{ JsonWebKey, JsonWebKeyAlgorithm, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, JsonWebTokenAlgorithm, PrivateSigningKey, SignatureVerificationError, SigningError, }; use ed25519_dalek::pkcs8::DecodePrivateKey; use ed25519_dalek::Signer; use rsa::pkcs1::DecodeRsaPrivateKey; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use sha2::{Digest, Sha256, Sha384, Sha512}; #[cfg(test)] mod tests; // Other than the 'kty' (key type) parameter, which must be present in all JWKs, Section 4 of RFC // 7517 states that "member names used for representing key parameters for different keys types // need not be distinct." Therefore, it's possible that future or non-standard key types will supply // some of the following parameters but with different types, causing deserialization to fail. To // support such key types, we'll need to define a new impl for JsonWebKey. Deserializing the new // impl would probably need to involve first deserializing the raw values to access the 'kty' // parameter, and then deserializing the fields and types appropriate for that key type. /// Public or symmetric key expressed as a JSON Web Key. #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct CoreJsonWebKey { pub(crate) kty: CoreJsonWebKeyType, #[serde(rename = "use")] pub(crate) use_: Option, pub(crate) kid: Option, /// The algorithm intended to be used with this key (see /// [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517#section-4.4)). /// /// It can either be an algorithm intended for use with JWS or JWE, or something different. pub(crate) alg: Option>, // From RFC 7517, Section 4: "Additional members can be present in the JWK; if not understood // by implementations encountering them, they MUST be ignored. Member names used for // representing key parameters for different keys types need not be distinct." // Hence, we set fields we fail to deserialize (understand) as None. #[serde(default, deserialize_with = "deserialize_option_or_none")] pub(crate) n: Option, #[serde(default, deserialize_with = "deserialize_option_or_none")] pub(crate) e: Option, //Elliptic Curve #[serde(default, deserialize_with = "deserialize_option_or_none")] pub(crate) crv: Option, #[serde(default, deserialize_with = "deserialize_option_or_none")] pub(crate) x: Option, #[serde(default, deserialize_with = "deserialize_option_or_none")] pub(crate) y: Option, #[serde(default, deserialize_with = "deserialize_option_or_none")] pub(crate) d: Option, // Used for symmetric keys, which we only generate internally from the client secret; these // are never part of the JWK set. #[serde(default, deserialize_with = "deserialize_option_or_none")] pub(crate) k: Option, } impl CoreJsonWebKey { /// Instantiate a new RSA public key from the raw modulus (`n`) and public exponent (`e`), /// along with an optional (but recommended) key ID. /// /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying /// their signatures. pub fn new_rsa(n: Vec, e: Vec, kid: Option) -> Self { Self { kty: CoreJsonWebKeyType::RSA, use_: Some(CoreJsonWebKeyUse::Signature), kid, n: Some(Base64UrlEncodedBytes::new(n)), e: Some(Base64UrlEncodedBytes::new(e)), k: None, crv: None, x: None, y: None, d: None, alg: None, } } /// Instantiate a new EC public key from the raw x (`x`) and y(`y`) part of the curve, /// along with an optional (but recommended) key ID. /// /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying /// their signatures. pub fn new_ec( x: Vec, y: Vec, crv: CoreJsonCurveType, kid: Option, ) -> Self { Self { kty: CoreJsonWebKeyType::EllipticCurve, use_: Some(CoreJsonWebKeyUse::Signature), kid, n: None, e: None, k: None, crv: Some(crv), x: Some(Base64UrlEncodedBytes::new(x)), y: Some(Base64UrlEncodedBytes::new(y)), d: None, alg: None, } } /// Instantiate a new Octet Key-Pair public key from the raw x (`x`) part of the curve, /// along with an optional (but recommended) key ID. /// /// The key ID is used for matching signed JSON Web Tokens with the keys used for verifying /// their signatures. pub fn new_okp(x: Vec, crv: CoreJsonCurveType, kid: Option) -> Self { Self { kty: CoreJsonWebKeyType::OctetKeyPair, use_: Some(CoreJsonWebKeyUse::Signature), kid, n: None, e: None, k: None, crv: Some(crv), x: Some(Base64UrlEncodedBytes::new(x)), y: None, d: None, alg: None, } } } impl JsonWebKey for CoreJsonWebKey { type KeyUse = CoreJsonWebKeyUse; type SigningAlgorithm = CoreJwsSigningAlgorithm; fn key_id(&self) -> Option<&JsonWebKeyId> { self.kid.as_ref() } fn key_type(&self) -> &CoreJsonWebKeyType { &self.kty } fn key_use(&self) -> Option<&CoreJsonWebKeyUse> { self.use_.as_ref() } fn signing_alg(&self) -> JsonWebKeyAlgorithm<&CoreJwsSigningAlgorithm> { match self.alg { None => JsonWebKeyAlgorithm::Unspecified, Some(JsonWebTokenAlgorithm::Signature(ref alg)) => JsonWebKeyAlgorithm::Algorithm(alg), Some(_) => JsonWebKeyAlgorithm::Unsupported, } } fn new_symmetric(key: Vec) -> Self { Self { kty: CoreJsonWebKeyType::Symmetric, use_: None, kid: None, n: None, e: None, k: Some(Base64UrlEncodedBytes::new(key)), crv: None, x: None, y: None, d: None, alg: None, } } fn verify_signature( &self, signature_alg: &CoreJwsSigningAlgorithm, message: &[u8], signature: &[u8], ) -> Result<(), SignatureVerificationError> { use hmac::Mac; check_key_compatibility(self, signature_alg) .map_err(|e| SignatureVerificationError::InvalidKey(e.to_owned()))?; match *signature_alg { CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 => { let message = { let mut hasher = sha2::Sha256::new(); hasher.update(message); &hasher.finalize() }; crypto::verify_rsa_signature( self, rsa::Pkcs1v15Sign::new::(), message, signature, ) } CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 => { let message = { let mut hasher = sha2::Sha384::new(); hasher.update(message); &hasher.finalize() }; crypto::verify_rsa_signature( self, rsa::Pkcs1v15Sign::new::(), message, signature, ) } CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 => { let message = { let mut hasher = sha2::Sha512::new(); hasher.update(message); &hasher.finalize() }; crypto::verify_rsa_signature( self, rsa::Pkcs1v15Sign::new::(), message, signature, ) } CoreJwsSigningAlgorithm::RsaSsaPssSha256 => { let message = { let mut hasher = sha2::Sha256::new(); hasher.update(message); &hasher.finalize() }; crypto::verify_rsa_signature( self, rsa::Pss::new::(), message, signature, ) } CoreJwsSigningAlgorithm::RsaSsaPssSha384 => { let message = { let mut hasher = sha2::Sha384::new(); hasher.update(message); &hasher.finalize() }; crypto::verify_rsa_signature( self, rsa::Pss::new::(), message, signature, ) } CoreJwsSigningAlgorithm::RsaSsaPssSha512 => { let message = { let mut hasher = sha2::Sha512::new(); hasher.update(message); &hasher.finalize() }; crypto::verify_rsa_signature( self, rsa::Pss::new::(), message, signature, ) } CoreJwsSigningAlgorithm::HmacSha256 => { let mut mac = hmac::Hmac::::new_from_slice( self.k.as_ref().ok_or_else(|| { SignatureVerificationError::InvalidKey( "Symmetric key `k` is missing".to_string(), ) })?, ) .map_err(|e| { SignatureVerificationError::Other(format!("Could not create key: {}", e)) })?; mac.update(message); mac.verify(signature.into()) .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) } CoreJwsSigningAlgorithm::HmacSha384 => { let mut mac = hmac::Hmac::::new_from_slice( self.k.as_ref().ok_or_else(|| { SignatureVerificationError::InvalidKey( "Symmetric key `k` is missing".to_string(), ) })?, ) .map_err(|e| { SignatureVerificationError::Other(format!("Could not create key: {}", e)) })?; mac.update(message); mac.verify(signature.into()) .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) } CoreJwsSigningAlgorithm::HmacSha512 => { let mut mac = hmac::Hmac::::new_from_slice( self.k.as_ref().ok_or_else(|| { SignatureVerificationError::InvalidKey( "Symmetric key `k` is missing".to_string(), ) })?, ) .map_err(|e| { SignatureVerificationError::Other(format!("Could not create key: {}", e)) })?; mac.update(message); mac.verify(signature.into()) .map_err(|_| SignatureVerificationError::CryptoError("bad HMAC".to_string())) } CoreJwsSigningAlgorithm::EcdsaP256Sha256 => { if matches!(self.crv, Some(CoreJsonCurveType::P256)) { crypto::verify_ec_signature(self, message, signature) } else { Err(SignatureVerificationError::InvalidKey( "Key uses different CRV than JWT".to_string(), )) } } CoreJwsSigningAlgorithm::EcdsaP384Sha384 => { if matches!(self.crv, Some(CoreJsonCurveType::P384)) { crypto::verify_ec_signature(self, message, signature) } else { Err(SignatureVerificationError::InvalidKey( "Key uses different CRV than JWT".to_string(), )) } } CoreJwsSigningAlgorithm::EdDsa => match self.crv { None => Err(SignatureVerificationError::InvalidKey( "EdDSA key must specify `crv`".to_string(), )), Some(CoreJsonCurveType::Ed25519) => { crypto::verify_ed_signature(self, message, signature) } Some(ref crv) => Err(SignatureVerificationError::InvalidKey(format!( "Unsupported EdDSA curve {crv:?}" ))), }, ref other => Err(SignatureVerificationError::UnsupportedAlg( serde_plain::to_string(other).unwrap_or_else(|err| { panic!( "signature alg {:?} failed to serialize to a string: {}", other, err ) }), )), } } fn hash_bytes(&self, bytes: &[u8], alg: &Self::SigningAlgorithm) -> Result, String> { check_key_compatibility(self, alg).map_err(String::from)?; match *alg { CoreJwsSigningAlgorithm::HmacSha256 | CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 | CoreJwsSigningAlgorithm::RsaSsaPssSha256 | CoreJwsSigningAlgorithm::EcdsaP256Sha256 => { let mut hasher = Sha256::new(); hasher.update(bytes); Ok(hasher.finalize().to_vec()) } CoreJwsSigningAlgorithm::HmacSha384 | CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 | CoreJwsSigningAlgorithm::RsaSsaPssSha384 | CoreJwsSigningAlgorithm::EcdsaP384Sha384 => { let mut hasher = Sha384::new(); hasher.update(bytes); Ok(hasher.finalize().to_vec()) } CoreJwsSigningAlgorithm::HmacSha512 | CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 | CoreJwsSigningAlgorithm::RsaSsaPssSha512 | CoreJwsSigningAlgorithm::EcdsaP521Sha512 => { let mut hasher = Sha512::new(); hasher.update(bytes); Ok(hasher.finalize().to_vec()) } CoreJwsSigningAlgorithm::EdDsa => match self.crv { None => Err("EdDSA key must specify `crv`".to_string()), Some(CoreJsonCurveType::Ed25519) => { let mut hasher = Sha512::new(); hasher.update(bytes); Ok(hasher.finalize().to_vec()) } Some(ref crv) => Err(format!("Unsupported EdDSA curve {crv:?}")), }, CoreJwsSigningAlgorithm::None => { Err("Signature algorithm `none` has no corresponding hash algorithm".to_string()) } } } } /// HMAC secret key. /// /// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying /// them. #[derive(Clone)] pub struct CoreHmacKey { secret: Vec, } impl CoreHmacKey { /// Instantiate a new key from the specified secret bytes. pub fn new(secret: T) -> Self where T: Into>, { Self { secret: secret.into(), } } } impl PrivateSigningKey for CoreHmacKey { type VerificationKey = CoreJsonWebKey; fn sign( &self, signature_alg: &CoreJwsSigningAlgorithm, message: &[u8], ) -> Result, SigningError> { use hmac::Mac; match *signature_alg { CoreJwsSigningAlgorithm::HmacSha256 => { let mut mac = hmac::Hmac::::new_from_slice(&self.secret) .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; mac.update(message); let result = mac.finalize(); Ok(result.into_bytes().as_slice().to_vec()) } CoreJwsSigningAlgorithm::HmacSha384 => { let mut mac = hmac::Hmac::::new_from_slice(&self.secret) .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; mac.update(message); let result = mac.finalize(); Ok(result.into_bytes().as_slice().to_vec()) } CoreJwsSigningAlgorithm::HmacSha512 => { let mut mac = hmac::Hmac::::new_from_slice(&self.secret) .map_err(|e| SigningError::Other(format!("Could not create key: {}", e)))?; mac.update(message); let result = mac.finalize(); Ok(result.into_bytes().as_slice().to_vec()) } ref other => Err(SigningError::UnsupportedAlg( serde_plain::to_string(other).unwrap_or_else(|err| { panic!( "signature alg {:?} failed to serialize to a string: {}", other, err ) }), )), } } fn as_verification_key(&self) -> CoreJsonWebKey { CoreJsonWebKey::new_symmetric(self.secret.clone()) } } enum EdDsaSigningKey { Ed25519(ed25519_dalek::SigningKey), } impl EdDsaSigningKey { fn from_ed25519_pem(pem: &str) -> Result { Ok(Self::Ed25519( ed25519_dalek::SigningKey::from_pkcs8_pem(pem).map_err(|err| err.to_string())?, )) } fn sign(&self, message: &[u8]) -> Vec { match self { Self::Ed25519(key) => { let signature = key.sign(message); signature.to_vec() } } } } /// EdDSA Private Key. /// /// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying /// them. pub struct CoreEdDsaPrivateSigningKey { kid: Option, key_pair: EdDsaSigningKey, } impl CoreEdDsaPrivateSigningKey { /// Converts an EdDSA private key (in PEM format) to a JWK representing its public key. pub fn from_ed25519_pem(pem: &str, kid: Option) -> Result { Ok(Self { kid, key_pair: EdDsaSigningKey::from_ed25519_pem(pem)?, }) } } impl PrivateSigningKey for CoreEdDsaPrivateSigningKey { type VerificationKey = CoreJsonWebKey; fn sign( &self, signature_alg: &CoreJwsSigningAlgorithm, message: &[u8], ) -> Result, SigningError> { match *signature_alg { CoreJwsSigningAlgorithm::EdDsa => Ok(self.key_pair.sign(message)), ref other => Err(SigningError::UnsupportedAlg( serde_plain::to_string(other).unwrap_or_else(|err| { panic!( "signature alg {:?} failed to serialize to a string: {}", other, err ) }), )), } } fn as_verification_key(&self) -> CoreJsonWebKey { match &self.key_pair { EdDsaSigningKey::Ed25519(key) => CoreJsonWebKey { kty: CoreJsonWebKeyType::OctetKeyPair, use_: Some(CoreJsonWebKeyUse::Signature), kid: self.kid.clone(), n: None, e: None, crv: Some(CoreJsonCurveType::Ed25519), x: Some(Base64UrlEncodedBytes::new( key.verifying_key().as_bytes().to_vec(), )), y: None, d: None, k: None, alg: None, }, } } } /// Trait used to allow testing with an alternative RNG. /// Clone is necessary to get a mutable version of the RNG. pub(crate) trait RngClone: dyn_clone::DynClone + rand::RngCore + rand::CryptoRng {} dyn_clone::clone_trait_object!(RngClone); impl RngClone for T where T: rand::RngCore + rand::CryptoRng + Clone {} /// RSA private key. /// /// This key can be used for signing messages, or converted to a `CoreJsonWebKey` for verifying /// them. pub struct CoreRsaPrivateSigningKey { key_pair: rsa::RsaPrivateKey, rng: Box, kid: Option, } impl CoreRsaPrivateSigningKey { /// Converts an RSA private key (in PEM format) to a JWK representing its public key. pub fn from_pem(pem: &str, kid: Option) -> Result { Self::from_pem_internal(pem, Box::new(rand::rngs::OsRng), kid) } pub(crate) fn from_pem_internal( pem: &str, rng: Box, kid: Option, ) -> Result { let key_pair = rsa::RsaPrivateKey::from_pkcs1_pem(pem).map_err(|err| err.to_string())?; Ok(Self { key_pair, rng, kid }) } } impl PrivateSigningKey for CoreRsaPrivateSigningKey { type VerificationKey = CoreJsonWebKey; fn sign( &self, signature_alg: &CoreJwsSigningAlgorithm, msg: &[u8], ) -> Result, SigningError> { match *signature_alg { CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 => { let mut hasher = sha2::Sha256::new(); hasher.update(msg); let hash = hasher.finalize().to_vec(); self.key_pair .sign_with_rng( &mut dyn_clone::clone_box(&self.rng), rsa::Pkcs1v15Sign::new::(), &hash, ) .map_err(|_| SigningError::CryptoError) } CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 => { let mut hasher = sha2::Sha384::new(); hasher.update(msg); let hash = hasher.finalize().to_vec(); self.key_pair .sign_with_rng( &mut dyn_clone::clone_box(&self.rng), rsa::Pkcs1v15Sign::new::(), &hash, ) .map_err(|_| SigningError::CryptoError) } CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 => { let mut hasher = sha2::Sha512::new(); hasher.update(msg); let hash = hasher.finalize().to_vec(); self.key_pair .sign_with_rng( &mut dyn_clone::clone_box(&self.rng), rsa::Pkcs1v15Sign::new::(), &hash, ) .map_err(|_| SigningError::CryptoError) } CoreJwsSigningAlgorithm::RsaSsaPssSha256 => { let mut hasher = sha2::Sha256::new(); hasher.update(msg); let hash = hasher.finalize().to_vec(); self.key_pair .sign_with_rng( &mut dyn_clone::clone_box(&self.rng), rsa::Pss::new_with_salt::(hash.len()), &hash, ) .map_err(|_| SigningError::CryptoError) } CoreJwsSigningAlgorithm::RsaSsaPssSha384 => { let mut hasher = sha2::Sha384::new(); hasher.update(msg); let hash = hasher.finalize().to_vec(); self.key_pair .sign_with_rng( &mut dyn_clone::clone_box(&self.rng), rsa::Pss::new_with_salt::(hash.len()), &hash, ) .map_err(|_| SigningError::CryptoError) } CoreJwsSigningAlgorithm::RsaSsaPssSha512 => { let mut hasher = sha2::Sha512::new(); hasher.update(msg); let hash = hasher.finalize().to_vec(); self.key_pair .sign_with_rng( &mut dyn_clone::clone_box(&self.rng), rsa::Pss::new_with_salt::(hash.len()), &hash, ) .map_err(|_| SigningError::CryptoError) } ref other => Err(SigningError::UnsupportedAlg( serde_plain::to_string(other).unwrap_or_else(|err| { panic!( "signature alg {:?} failed to serialize to a string: {}", other, err ) }), )), } } fn as_verification_key(&self) -> CoreJsonWebKey { use rsa::traits::PublicKeyParts; let public_key = self.key_pair.to_public_key(); CoreJsonWebKey { kty: CoreJsonWebKeyType::RSA, use_: Some(CoreJsonWebKeyUse::Signature), kid: self.kid.clone(), n: Some(Base64UrlEncodedBytes::new(public_key.n().to_bytes_be())), e: Some(Base64UrlEncodedBytes::new(public_key.e().to_bytes_be())), k: None, crv: None, x: None, y: None, d: None, alg: None, } } } /// Type of JSON Web Key. #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[non_exhaustive] pub enum CoreJsonWebKeyType { /// Elliptic Curve Cryptography (ECC) key. /// /// ECC algorithms such as ECDSA are currently unsupported. #[serde(rename = "EC")] EllipticCurve, /// RSA key. #[serde(rename = "RSA")] RSA, /// EdDSA key. #[serde(rename = "OKP")] OctetKeyPair, /// Symmetric key. #[serde(rename = "oct")] Symmetric, } impl JsonWebKeyType for CoreJsonWebKeyType {} /// Type of EC-Curve #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[non_exhaustive] pub enum CoreJsonCurveType { /// P-256 Curve #[serde(rename = "P-256")] P256, /// P-384 Curve #[serde(rename = "P-384")] P384, /// P-521 Curve (currently not supported) #[serde(rename = "P-521")] P521, /// Ed25519 Curve #[serde(rename = "Ed25519")] Ed25519, } /// Usage restriction for a JSON Web key. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CoreJsonWebKeyUse { /// Key may be used for digital signatures. Signature, /// Key may be used for encryption. Encryption, /// Fallback case for other key uses not understood by this library. Other(String), } impl CoreJsonWebKeyUse { fn from_str(s: &str) -> Self { match s { "sig" => Self::Signature, "enc" => Self::Encryption, other => Self::Other(other.to_string()), } } } impl AsRef for CoreJsonWebKeyUse { fn as_ref(&self) -> &str { match self { CoreJsonWebKeyUse::Signature => "sig", CoreJsonWebKeyUse::Encryption => "enc", CoreJsonWebKeyUse::Other(other) => other.as_str(), } } } impl JsonWebKeyUse for CoreJsonWebKeyUse { fn allows_signature(&self) -> bool { matches!(*self, CoreJsonWebKeyUse::Signature) } fn allows_encryption(&self) -> bool { matches!(*self, CoreJsonWebKeyUse::Encryption) } } // FIXME: Once https://github.com/serde-rs/serde/issues/912 is resolved, use #[serde(other)] instead // of custom serializer/deserializers. Right now this isn't possible because serde(other) only // supports unit variants. deserialize_from_str!(CoreJsonWebKeyUse); serialize_as_str!(CoreJsonWebKeyUse); ================================================ FILE: src/core/jwk/tests.rs ================================================ use crate::core::jwk::CoreJsonCurveType; use crate::core::{ CoreEdDsaPrivateSigningKey, CoreHmacKey, CoreJsonWebKey, CoreJsonWebKeySet, CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, }; use crate::helpers::Base64UrlEncodedBytes; use crate::jwt::tests::{ TEST_EC_PUB_KEY_P256, TEST_EC_PUB_KEY_P384, TEST_ED_PUB_KEY_ED25519, TEST_RSA_PUB_KEY, }; use crate::verification::SignatureVerificationError; use crate::{JsonWebKey, JsonWebKeyId, JsonWebTokenAlgorithm, PrivateSigningKey, SigningError}; use base64::prelude::BASE64_STANDARD; use base64::Engine; use rand::rngs::mock::StepRng; use rand::{CryptoRng, RngCore}; use rsa::rand_core; #[test] fn test_core_jwk_deserialization_rsa() { let json = "{ \"kty\": \"RSA\", \"use\": \"sig\", \"kid\": \"2011-04-29\", \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ jF44-csFCur-kEgU8awapJzKnqDKgw\", \"e\": \"AQAB\" }"; let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); assert_eq!(key.kty, CoreJsonWebKeyType::RSA); assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); assert_eq!(key.kid, Some(JsonWebKeyId::new("2011-04-29".to_string()))); assert_eq!( key.n, Some(Base64UrlEncodedBytes::new(vec![ 210, 252, 123, 106, 10, 30, 108, 103, 16, 74, 235, 143, 136, 178, 87, 102, 155, 77, 246, 121, 221, 173, 9, 155, 92, 74, 108, 217, 168, 128, 21, 181, 161, 51, 191, 11, 133, 108, 120, 113, 182, 223, 0, 11, 85, 79, 206, 179, 194, 237, 81, 43, 182, 143, 20, 92, 110, 132, 52, 117, 47, 171, 82, 161, 207, 193, 36, 64, 143, 121, 181, 138, 69, 120, 193, 100, 40, 133, 87, 137, 247, 162, 73, 227, 132, 203, 45, 159, 174, 45, 103, 253, 150, 251, 146, 108, 25, 142, 7, 115, 153, 253, 200, 21, 192, 175, 9, 125, 222, 90, 173, 239, 244, 77, 231, 14, 130, 127, 72, 120, 67, 36, 57, 191, 238, 185, 96, 104, 208, 71, 79, 197, 13, 109, 144, 191, 58, 152, 223, 175, 16, 64, 200, 156, 2, 214, 146, 171, 59, 60, 40, 150, 96, 157, 134, 253, 115, 183, 116, 206, 7, 64, 100, 124, 238, 234, 163, 16, 189, 18, 249, 133, 168, 235, 159, 89, 253, 212, 38, 206, 165, 178, 18, 15, 79, 42, 52, 188, 171, 118, 75, 126, 108, 84, 214, 132, 2, 56, 188, 196, 5, 135, 165, 158, 102, 237, 31, 51, 137, 69, 119, 99, 92, 71, 10, 247, 92, 249, 44, 32, 209, 218, 67, 225, 191, 196, 25, 226, 34, 166, 240, 208, 187, 53, 140, 94, 56, 249, 203, 5, 10, 234, 254, 144, 72, 20, 241, 172, 26, 164, 156, 202, 158, 160, 202, 131, ])) ); assert_eq!(key.e, Some(Base64UrlEncodedBytes::new(vec![1, 0, 1]))); assert_eq!(key.k, None); } #[test] fn test_core_jwk_deserialization_ec() { let json = "{ \"kty\": \"EC\", \"use\": \"sig\", \"kid\": \"2011-04-29\", \"crv\": \"P-256\", \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" }"; let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); assert_eq!(key.kty, CoreJsonWebKeyType::EllipticCurve); assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); assert_eq!(key.kid, Some(JsonWebKeyId::new("2011-04-29".to_string()))); assert_eq!(key.crv, Some(CoreJsonCurveType::P256)); assert_eq!( key.y, Some(Base64UrlEncodedBytes::new(vec![ 0x4a, 0xd0, 0xef, 0x28, 0x88, 0x17, 0xa8, 0x0c, 0x49, 0xe8, 0x3b, 0x9e, 0x6d, 0x11, 0x21, 0xfb, 0x5b, 0xe0, 0xbd, 0x94, 0x56, 0xdd, 0xd7, 0xeb, 0x3b, 0x14, 0x88, 0x2b, 0x30, 0x6d, 0x44, 0x8d ])) ); assert_eq!( key.x, Some(Base64UrlEncodedBytes::new(vec![ 0x91, 0x70, 0x86, 0x64, 0x8a, 0xf7, 0xa0, 0x8e, 0xac, 0x29, 0xb9, 0xd3, 0xea, 0xb4, 0x6c, 0x21, 0xdc, 0x45, 0x5f, 0x0d, 0xff, 0x55, 0xb2, 0xe4, 0xfd, 0xcb, 0xde, 0x6a, 0x38, 0x2a, 0x5e, 0x4f ])) ); } #[test] fn test_core_jwk_deserialization_ed() { let json = "{ \"alg\": \"EdDSA\", \"crv\": \"Ed25519\", \"kty\": \"OKP\", \"use\": \"sig\", \"x\": \"vZ3CX884r0qNJ18pgXUTvFufK3ZmDzQfvMROJz6CLBc\" }"; let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); assert_eq!(key.kty, CoreJsonWebKeyType::OctetKeyPair); assert_eq!(key.use_, Some(CoreJsonWebKeyUse::Signature)); assert_eq!(key.crv, Some(CoreJsonCurveType::Ed25519)); assert_eq!( key.x, Some(Base64UrlEncodedBytes::new(vec![ 0xBD, 0x9D, 0xC2, 0x5F, 0xCF, 0x38, 0xAF, 0x4A, 0x8D, 0x27, 0x5F, 0x29, 0x81, 0x75, 0x13, 0xBC, 0x5B, 0x9F, 0x2B, 0x76, 0x66, 0x0F, 0x34, 0x1F, 0xBC, 0xC4, 0x4E, 0x27, 0x3E, 0x82, 0x2C, 0x17 ])) ); } #[test] fn test_core_jwk_deserialization_symmetric() { let json = "{\ \"kty\":\"oct\", \"alg\":\"A128GCM\", \"k\":\"GawgguFyGrWKav7AX4VKUg\" }"; let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); assert_eq!(key.use_, None); assert_eq!(key.kid, None); assert_eq!(key.n, None); assert_eq!(key.e, None); assert_eq!( key.alg, Some(JsonWebTokenAlgorithm::Encryption( CoreJweContentEncryptionAlgorithm::Aes128Gcm )) ); assert_eq!( key.k, Some(Base64UrlEncodedBytes::new(vec![ 25, 172, 32, 130, 225, 114, 26, 181, 138, 106, 254, 192, 95, 133, 74, 82, ])) ); } #[test] fn test_core_jwk_deserialization_no_optional() { let json = "{\"kty\":\"oct\"}"; let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); assert_eq!(key.use_, None); assert_eq!(key.kid, None); assert_eq!(key.n, None); assert_eq!(key.e, None); assert_eq!(key.k, None); } #[test] fn test_core_jwk_deserialization_unrecognized() { // Unrecognized fields should be ignored during deserialization let json = "{\ \"kty\": \"oct\", \"unrecognized\": 1234 }"; let key: CoreJsonWebKey = serde_json::from_str(json).expect("deserialization failed"); assert_eq!(key.kty, CoreJsonWebKeyType::Symmetric); } #[test] fn test_core_jwk_deserialization_dupe_fields() { // From RFC 7517, Section 4: // "The member names within a JWK MUST be unique; JWK parsers MUST either // reject JWKs with duplicate member names or use a JSON parser that // returns only the lexically last duplicate member name, as specified // in Section 15.12 (The JSON Object) of ECMAScript 5.1 [ECMAScript]." let json = "{\ \"kty\":\"oct\", \"k\":\"GawgguFyGrWKav7AX4VKUg\", \"k\":\"GawgguFyGrWKav7AX4VKVg\" }"; assert!(serde_json::from_str::(json) .expect_err("deserialization must fail when duplicate fields are present") .to_string() // This is probably not ideal since the serde/serde_json contracts don't guarantee this // error message. However, we want to be sure that this fails for the expected reason // and not by happenstance, so this is fine for now. .contains("duplicate field")); } fn verify_signature( key: &CoreJsonWebKey, alg: &CoreJwsSigningAlgorithm, signing_input: &str, signature_base64: &str, ) { let signature = crate::core::base64_url_safe_no_pad() .decode(signature_base64) .expect("failed to base64url decode"); key.verify_signature(alg, signing_input.as_bytes(), &signature) .expect("signature verification failed"); match key .verify_signature( alg, (signing_input.to_string() + "foobar").as_bytes(), &signature, ) .expect_err("signature verification should fail") { SignatureVerificationError::CryptoError(_) => {} other => panic!("unexpected error: {:?}", other), } } fn verify_invalid_signature( key: &CoreJsonWebKey, alg: &CoreJwsSigningAlgorithm, signing_input: &str, signature_base64: &str, ) { let signature = crate::core::base64_url_safe_no_pad() .decode(signature_base64) .expect("failed to base64url decode"); match key .verify_signature(alg, signing_input.as_bytes(), &signature) .expect_err("signature verification should fail") { SignatureVerificationError::CryptoError(_) => {} other => panic!("unexpected error: {:?}", other), } } #[test] fn test_eddsa_verification() { let key_ed25519: CoreJsonWebKey = serde_json::from_str(TEST_ED_PUB_KEY_ED25519).expect("deserialization failed"); let pkcs1_signing_input = "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIn0.eyJpc3MiOiJqb2UifQ"; let signature_ed25519 = "Augr7UH6hUbWVN0PHqSD5U0bb8y9UOw_eef09ZS5d5haUar_qAto8gyLJxUhNF5wHPoXhdvSGowkPvjiKsEsCQ"; let signature_ed25519_other = "xb4NH-q33sCaRXf1ZhnzQxd4o5ZkBWKd9vGibacqPMAblW_mIJLm9kGerqHX08SPoeDY-dYUmZQz9ls6csfvAw"; let signature_ed448 = "xxXVMyaYYePdGfMOdU0nENuc70pKwP3vJuc_jBA0rCW-RtbvBLSsc0D9iCPzhrPmQ2X1nTjPkGiAXJ0_NslDBvy3sHu88N64YhnnYBWwwHttBU0jijn_ikbBUHzUwzGuasRFb1ESG_PwedhEcMi-YAwA"; // test ed25519 verify_signature( &key_ed25519, &CoreJwsSigningAlgorithm::EdDsa, pkcs1_signing_input, signature_ed25519, ); // signature from ed448 variant verify_invalid_signature( &key_ed25519, &CoreJwsSigningAlgorithm::EdDsa, pkcs1_signing_input, signature_ed448, ); // different signature verify_invalid_signature( &key_ed25519, &CoreJwsSigningAlgorithm::EdDsa, pkcs1_signing_input, signature_ed25519_other, ); // non-EdDsa key if let Some(err) = key_ed25519 .verify_signature( &CoreJwsSigningAlgorithm::EcdsaP256Sha256, pkcs1_signing_input.as_bytes(), signature_ed25519.as_bytes(), ) .err() { let error_msg = "key type does not match signature algorithm".to_string(); match err { SignatureVerificationError::InvalidKey(msg) => { if msg != error_msg { panic!("The error should be about key type") } } _ => panic!("We should fail before actual validation"), } } } #[test] fn test_ecdsa_verification() { let key_p256: CoreJsonWebKey = serde_json::from_str(TEST_EC_PUB_KEY_P256).expect("deserialization failed"); let key_p384: CoreJsonWebKey = serde_json::from_str(TEST_EC_PUB_KEY_P384).expect("deserialization failed"); let pkcs1_signing_input = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\ hhbXBsZSJ9.\ SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; let signature_p256 = "EnKCtAHhzhqxV2GTr1VEurse2kQ7oHpFoVqM66sYGlmahDRGSlfrVAsGCzdLv66OS2Qf1zt6OPHX-5ZAkMgzlA"; let signature_p384 = "B_9oDAabMasZ2Yt_cnAS21owaN0uWSInQBPxTqqiM3N3XjkksBRMGqguJLV5WoSMcvqgXwHTTQtbHGuh0Uf4g6LEr7XtO1T2KCttQR27d5YbvVZdORrzCm0Nsm1zkV-i"; //test p256 verify_signature( &key_p256, &CoreJwsSigningAlgorithm::EcdsaP256Sha256, pkcs1_signing_input, signature_p256, ); //wrong algo should fail before ring validation if let Some(err) = key_p256 .verify_signature( &CoreJwsSigningAlgorithm::EcdsaP384Sha384, pkcs1_signing_input.as_bytes(), signature_p256.as_bytes(), ) .err() { let error_msg = "Key uses different CRV than JWT".to_string(); match err { SignatureVerificationError::InvalidKey(msg) => { if msg != error_msg { panic!("The error should be about different CRVs") } } _ => panic!("We should fail before actual validation"), } } // suppose we have alg specified correctly, but the signature given is actually a p384 verify_invalid_signature( &key_p256, &CoreJwsSigningAlgorithm::EcdsaP256Sha256, pkcs1_signing_input, signature_p384, ); //test p384 verify_signature( &key_p384, &CoreJwsSigningAlgorithm::EcdsaP384Sha384, pkcs1_signing_input, signature_p384, ); // suppose we have alg specified correctly, but the signature given is actually a p256 verify_invalid_signature( &key_p384, &CoreJwsSigningAlgorithm::EcdsaP384Sha384, pkcs1_signing_input, signature_p256, ); //wrong algo should fail before ring validation if let Some(err) = key_p384 .verify_signature( &CoreJwsSigningAlgorithm::EcdsaP256Sha256, pkcs1_signing_input.as_bytes(), signature_p384.as_bytes(), ) .err() { let error_msg = "Key uses different CRV than JWT".to_string(); match err { SignatureVerificationError::InvalidKey(msg) => { if msg != error_msg { panic!("The error should be about different CRVs") } } _ => panic!("We should fail before actual validation"), } } } #[test] fn test_rsa_pkcs1_verification() { let key: CoreJsonWebKey = serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); // Source: https://tools.ietf.org/html/rfc7520#section-4.1 let pkcs1_signing_input = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZX\ hhbXBsZSJ9.\ SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; verify_signature( &key, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, pkcs1_signing_input, "MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\ ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\ IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\ W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\ xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\ cIe8u9ipH84ogoree7vjbU5y18kDquDg", ); verify_signature( &key, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, pkcs1_signing_input, "dgTHNAePceEDFodrPybExGb2aF4fHb4bRpb_4bgYHq78fUdHFCScg0bZP51zjB\ joH-4fr0P7Y8-Sns0GuXRy_itY2Yh0mEdXVn6HwZVOGIVRAuBkY0cAgSXGKU40\ 1G-GhamiNyNDfN2bwHftPPvCdsChtsLeAUvhWUKSLgIfT-jvMr9iZ5d0SQrUvv\ G1ReEoBDyKUzqGQehO3CNGJ-QkI8p-fBTa2KHQxct6cU5_anSXCd-kC2rtEQS9\ E8AcMFLA2Bv9IXsURBRU_bwMgxTG8c6ATDJM8k-zJSSP5a44EFKHUtH1xspYFo\ KV6Za-frCV8kcFCILMf-4ATlj5Z62o1A", ); verify_signature( &key, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, pkcs1_signing_input, "hIRFVu3hlbIM9Xt2V9xldCoF_94BEDg-6kVetoceakgD-9hicX0BnOI3YxR-JQ\ 0to4saNEdGP1ulvanfa5uK3PnltQr1sJ1l1x_TPNh8vdvZ5WmAtkQcZvRiK580\ hliHV1l65yLyGH4ckDicOg5VF4BASkBw6sUO_LCB8pMJotK5jQxDbNkPmSGbFV\ nzVXXy6QI_r6nqmguo5DMFlPeploS-aQ7ArfYqR3gKEp3l5gWWKn86lwVKRGjv\ zeRMf3ubhKxvHUyU8cE5p1VPpOzTJ3cPwUe68s24Ehf2jpgZIIXb9XQv4L0Unf\ GAXTBY7Rszx9LvGByoFx3eOpbMvtLQxA", ); // Wrong key type match key .verify_signature( &CoreJwsSigningAlgorithm::EcdsaP256Sha256, pkcs1_signing_input.as_bytes(), &Vec::new(), ) .expect_err("signature verification should fail") { SignatureVerificationError::InvalidKey(_) => {} other => panic!("unexpected error: {:?}", other), } // Wrong key usage let enc_key_json = "{ \"kty\": \"RSA\", \"kid\": \"bilbo.baggins@hobbiton.example\", \"use\": \"enc\", \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ HdrNP5zw\", \"e\": \"AQAB\" }"; let enc_key: CoreJsonWebKey = serde_json::from_str(enc_key_json).expect("deserialization failed"); match enc_key .verify_signature( &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, pkcs1_signing_input.as_bytes(), &Vec::new(), ) .expect_err("signature verification should fail") { SignatureVerificationError::InvalidKey(_) => {} other => panic!("unexpected error: {:?}", other), } // Key without usage specified should work let nousage_key_json = "{ \"kty\": \"RSA\", \"kid\": \"bilbo.baggins@hobbiton.example\", \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ HdrNP5zw\", \"e\": \"AQAB\" }"; let nousage_key: CoreJsonWebKey = serde_json::from_str(nousage_key_json).expect("deserialization failed"); verify_signature( &nousage_key, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, pkcs1_signing_input, "MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmK\ ZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4J\ IwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8w\ W1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluP\ xUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41Axf_f\ cIe8u9ipH84ogoree7vjbU5y18kDquDg", ); } #[test] fn test_rsa_pss_verification() { let key: CoreJsonWebKey = serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); // Source: https://tools.ietf.org/html/rfc7520#section-4.2 let pss_signing_input = "eyJhbGciOiJQUzM4NCIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.\ SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; verify_signature( &key, &CoreJwsSigningAlgorithm::RsaSsaPssSha256, pss_signing_input, "Y62we_hs07d0qJ2cT_QpbrodwDhPK9rEpNX2b3GqLHFM18YtDlPCr40Xf_yLIosIrt\ mMP4NgDSCkn2qOcRJBD8zrHumER4JIkGZbRIwU8gYms8xKX2HaveK9vrOjbHoWLjOU\ nyNpprYUFGdRZ6oebT61bqU2CZrJG_GcqR87W8FOn7kqrCPI7B8oNHgliMke49hOpz\ mluL20BKN5Mb3O42nwgmiONZK0Pjm2GTIAYRUvNQ741aCWVJ3rnWvo99qWhe86ap_H\ v40SUSaMwJig5AqC-wHIzYaYU0PlQbi83Dgw7Zft9kL2dGB0vMWY_h2HDgZU0teAcK\ SkhyH8ZDRyYQ", ); verify_signature( &key, &CoreJwsSigningAlgorithm::RsaSsaPssSha384, pss_signing_input, "cu22eBqkYDKgIlTpzDXGvaFfz6WGoz7fUDcfT0kkOy42miAh2qyBzk1xEsnk2I\ pN6-tPid6VrklHkqsGqDqHCdP6O8TTB5dDDItllVo6_1OLPpcbUrhiUSMxbbXU\ vdvWXzg-UD8biiReQFlfz28zGWVsdiNAUf8ZnyPEgVFn442ZdNqiVJRmBqrYRX\ e8P_ijQ7p8Vdz0TTrxUeT3lm8d9shnr2lfJT8ImUjvAA2Xez2Mlp8cBE5awDzT\ 0qI0n6uiP1aCN_2_jLAeQTlqRHtfa64QQSUmFAAjVKPbByi7xho0uTOcbH510a\ 6GYmJUAfmWjwZ6oD4ifKo8DYM-X72Eaw", ); verify_signature( &key, &CoreJwsSigningAlgorithm::RsaSsaPssSha512, pss_signing_input, "G8vtysTFbSXht_PU6NdXeYDOSIQhxcp6zFWuvtx2NCtgsm-J22CKqlapp1zjPkXTo4\ xrYlIgFjQVQZ9Cr7KWJXK7qYUkdfJNkB1E96EQR32ocx_9RQDS_eQNlGWjoDRduD9z\ 2hKs-S0EhOy39wUeUYbcKA1MpkW71hUPI56Ou5kzclNbe22slB4mYd6Mx0dLOeFDF2\ C7ZUDxso-cHMh4hU2E8vlp-TZUf9eqAri9T1F_pjRF8WNBj-vrqwy3bCROgIslYA8u\ c_FEXn6fZ21up5mU9vg5_LdeBoSh4Idmz8HLn5rpVd57AsQ2PbLMsKXcpVUhwP_ID1\ 7zsAFuCEFJqA", ); } #[test] fn test_hmac_sha256_verification() { // the original spec example also has alg=HS256, which was removed to test other signing algorithms let key_json = "{ \"kty\": \"oct\", \"kid\": \"018c0ae5-4d9b-471b-bfd6-eef314bc7037\", \"use\": \"sig\", \"k\": \"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg\" }"; let key: CoreJsonWebKey = serde_json::from_str(key_json).expect("deserialization failed"); // Source: https://tools.ietf.org/html/rfc7520#section-4.4 let signing_input = "eyJhbGciOiJIUzI1NiIsImtpZCI6IjAxOGMwYWU1LTRkOWItNDcxYi1iZmQ2LW\ VlZjMxNGJjNzAzNyJ9.\ SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IH\ lvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGUgcm9hZCwgYW5kIGlmIHlvdSBk\ b24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcm\ UgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4"; verify_signature( &key, &CoreJwsSigningAlgorithm::HmacSha256, signing_input, "s0h6KThzkfBBBkLspW1h84VsJZFTsPPqMDA7g1Md7p0", ); verify_signature( &key, &CoreJwsSigningAlgorithm::HmacSha384, signing_input, "O1jhTTHkuaiubwDZoIBLv6zjEarXHc22NNu05IdYh_yzIKGYXJQcaI2WnF4BCq7j", ); verify_signature( &key, &CoreJwsSigningAlgorithm::HmacSha512, signing_input, "rdWYqzXuAJp4OW-exqIwrO8HJJQDYu0_fkTIUBHmyHMFJ0pVe7fjP7QtE7BaX-7FN5\ YiyiM11MwIEAxzxBj6qw", ); } fn expect_hmac( secret_key: &CoreHmacKey, message: &[u8], alg: &CoreJwsSigningAlgorithm, expected_sig_base64: &str, ) { let sig = secret_key.sign(alg, message).unwrap(); assert_eq!(expected_sig_base64, BASE64_STANDARD.encode(&sig)); secret_key .as_verification_key() .verify_signature(alg, message, &sig) .unwrap(); } #[test] fn test_hmac_signing() { let secret_key = CoreHmacKey::new("my_secret_key"); let message = "hello HMAC".as_ref(); expect_hmac( &secret_key, message, &CoreJwsSigningAlgorithm::HmacSha256, "Pm6UhOcfx6D8LeCG4taMQNQXDTHwnVOSEcB7tidkM2M=", ); expect_hmac( &secret_key, message, &CoreJwsSigningAlgorithm::HmacSha384, "BiYrxF0XjImSnfqT2n+Tu3EspstKZmVtUHbK77LHerfKNwCikuClNJDAVwr2xMLp", ); expect_hmac( &secret_key, message, &CoreJwsSigningAlgorithm::HmacSha512, "glKjDMXBhB6sSKGCdLW4QeBOJ3vOgOlbMJjbeus8/KQ3dk7dtsqtrpfoDoW8lrU+rncd2jBWaKnp1zKdpEfSn\ A==", ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, message), Err(SigningError::UnsupportedAlg("RS256".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, message), Err(SigningError::UnsupportedAlg("RS384".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, message), Err(SigningError::UnsupportedAlg("RS512".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha256, message), Err(SigningError::UnsupportedAlg("PS256".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha384, message), Err(SigningError::UnsupportedAlg("PS384".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::RsaSsaPssSha512, message), Err(SigningError::UnsupportedAlg("PS512".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP256Sha256, message), Err(SigningError::UnsupportedAlg("ES256".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP384Sha384, message), Err(SigningError::UnsupportedAlg("ES384".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::EcdsaP521Sha512, message), Err(SigningError::UnsupportedAlg("ES512".to_string())), ); assert_eq!( secret_key.sign(&CoreJwsSigningAlgorithm::None, message), Err(SigningError::UnsupportedAlg("none".to_string())), ); } const TEST_ED25519_KEY: &str = "\ -----BEGIN PRIVATE KEY-----\n\ MC4CAQAwBQYDK2VwBCIEICWeYPLxoZKHZlQ6rkBi11E9JwchynXtljATLqym/XS9\n\ -----END PRIVATE KEY-----\ "; // This is just a test key that isn't used for anything else. const TEST_RSA_KEY: &str = "\ -----BEGIN RSA PRIVATE KEY-----\n\ MIIEowIBAAKCAQEAsRMj0YYjy7du6v1gWyKSTJx3YjBzZTG0XotRP0IaObw0k+68\n\ 30dXadjL5jVhSWNdcg9OyMyTGWfdNqfdrS6ppBqlQNgjZJdloIqL9zOLBZrDm7G4\n\ +qN4KeZ4/5TyEilq2zOHHGFEzXpOq/UxqVnm3J4fhjqCNaS2nKd7HVVXGBQQ+4+F\n\ dVT+MyJXemw5maz2F/h324TQi6XoUPEwUddxBwLQFSOlzWnHYMc4/lcyZJ8MpTXC\n\ MPe/YJFNtb9CaikKUdf8x4mzwH7usSf8s2d6R4dQITzKrjrEJ0u3w3eGkBBapoMV\n\ FBGPjP3Haz5FsVtHc5VEN3FZVIDF6HrbJH1C4QIDAQABAoIBAHSS3izM+3nc7Bel\n\ 8S5uRxRKmcm5je6b11u6qiVUFkHWJmMRc6QmqmSThkCq+b4/vUAe1cYZ7+l02Exo\n\ HOcrZiEULaDP6hUKGqyjKVv3wdlRtt8kFFxlC/HBufzAiNDuFVvzw0oquwnvMCXC\n\ yQvtlK+/JY/PqvM32cSt+b4o9apySsHqAtdsoHHohK82jsQqIfCi1v8XYV/xRBJB\n\ cQMCaA0Ls3tFpmJv3JdikyyQxio4kZ5tswghC63znCp1iL+qDq1wjjKzjick9MDb\n\ Qzb95X09QQP201l1FPWN7Kbhj4ybg6PJGz/VHQcvILcBCoYIc0UY/OMSBt9VN9yD\n\ wr1WlbECgYEA37difsTMcLmUEN57sicFe1q4lxH6eqnUBjmoKBflx4oMIIyRnfjF\n\ Jwsu9yIiBkJfBCP85nl2tZdcV0wfZLf6amxB/KMtdfW6r8eoTDzE472OYxSIg1F5\n\ dI4qn2nBI0Dou0g58xj+Kv0iLaym0pxtyJkSg/rxZGwKb9a+x5WAs50CgYEAyqC0\n\ NcZs2BRIiT5kEOF6+MeUvarbKh1mangKHKcTdXRrvoJ+Z5izm7FifBixo/79MYpt\n\ 0VofW0IzYKtAI9KZDq2JcozEbZ+lt/ZPH5QEXO4T39QbDoAG8BbOmEP7l+6m+7QO\n\ PiQ0WSNjDnwk3W7Zihgg31DH7hyxsxQCapKLcxUCgYAwERXPiPcoDSd8DGFlYK7z\n\ 1wUsKEe6DT0p7T9tBd1v5wA+ChXLbETn46Y+oQ3QbHg/yn+vAU/5KkFD3G4uVL0w\n\ Gnx/DIxa+OYYmHxXjQL8r6ClNycxl9LRsS4FPFKsAWk/u///dFI/6E1spNjfDY8k\n\ 94ab5tHwsqn3Z5tsBHo3nQKBgFUmxbSXh2Qi2fy6+GhTqU7k6G/wXhvLsR9rBKzX\n\ 1YiVfTXZNu+oL0ptd/q4keZeIN7x0oaY/fZm0pp8PP8Q4HtXmBxIZb+/yG+Pld6q\n\ YE8BSd7VDu3ABapdm0JHx3Iou4mpOBcLNeiDw3vx1bgsfkTXMPFHzE0XR+H+tak9\n\ nlalAoGBALAmAF7WBGdOt43Rj8hPaKOM/ahj+6z3CNwVreToNsVBHoyNmiO8q7MC\n\ +tRo4jgdrzk1pzs66OIHfbx5P1mXKPtgPZhvI5omAY8WqXEgeNqSL1Ksp6LZ2ql/\n\ ouZns5xwKc9+aRL+GWoAGNzwzcjE8cP52sBy/r0rYXTs/sZo5kgV\n\ -----END RSA PRIVATE KEY-----\ "; fn expect_ed_sig( private_key: &CoreEdDsaPrivateSigningKey, message: &[u8], alg: &CoreJwsSigningAlgorithm, expected_sig_base64: &str, ) { let sig = private_key.sign(alg, message).unwrap(); assert_eq!(expected_sig_base64, BASE64_STANDARD.encode(&sig)); let public_key = private_key.as_verification_key(); public_key.verify_signature(alg, message, &sig).unwrap(); } fn expect_rsa_sig( private_key: &CoreRsaPrivateSigningKey, message: &[u8], alg: &CoreJwsSigningAlgorithm, expected_sig_base64: &str, ) { let sig = private_key.sign(alg, message).unwrap(); assert_eq!(expected_sig_base64, BASE64_STANDARD.encode(&sig)); let public_key = private_key.as_verification_key(); public_key.verify_signature(alg, message, &sig).unwrap(); } #[derive(Clone)] struct TestRng(StepRng); impl CryptoRng for TestRng {} impl RngCore for TestRng { fn next_u32(&mut self) -> u32 { self.0.next_u32() } fn next_u64(&mut self) -> u64 { self.0.next_u64() } fn fill_bytes(&mut self, dest: &mut [u8]) { self.0.fill_bytes(dest) } fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { self.0.try_fill_bytes(dest) } } #[test] fn test_ed_signing() { let private_key = CoreEdDsaPrivateSigningKey::from_ed25519_pem( TEST_ED25519_KEY, Some(JsonWebKeyId::new("test_key".to_string())), ) .unwrap(); let public_key_jwk = private_key.as_verification_key(); let public_key_jwk_str = serde_json::to_string(&public_key_jwk).unwrap(); assert_eq!( "{\ \"kty\":\"OKP\",\ \"use\":\"sig\",\ \"kid\":\"test_key\",\ \"crv\":\"Ed25519\",\ \"x\":\"E6lXdyel1n9C1lcr3FK8OsfsfO2ZgcWhPflJ6yIf7e8\"\ }", public_key_jwk_str ); let message = "hello EdDsa".as_ref(); expect_ed_sig( &private_key, message, &CoreJwsSigningAlgorithm::EdDsa, "XqP8sXaPrQa37+2lw+aiXv+6pegjioYUgo1/ShcX6kRhD2Vxh8DrQUbQlaGbljLJTNNc453E2Axp+Mxm+4OVAQ==", ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::HmacSha256, message), Err(SigningError::UnsupportedAlg("HS256".to_string())), ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::None, message), Err(SigningError::UnsupportedAlg("none".to_string())), ); } #[test] fn test_rsa_signing() { let private_key = CoreRsaPrivateSigningKey::from_pem_internal( TEST_RSA_KEY, // Constant salt used for PSS test vectors below. Box::new(TestRng(StepRng::new(127, 0))), Some(JsonWebKeyId::new("test_key".to_string())), ) .unwrap(); let public_key_jwk = private_key.as_verification_key(); let public_key_jwk_str = serde_json::to_string(&public_key_jwk).unwrap(); assert_eq!( "{\ \"kty\":\"RSA\",\ \"use\":\"sig\",\ \"kid\":\"test_key\",\ \"n\":\"sRMj0YYjy7du6v1gWyKSTJx3YjBzZTG0XotRP0IaObw0k-6830dXadjL5jVhSWNdcg9OyMyTGWfdNq\ fdrS6ppBqlQNgjZJdloIqL9zOLBZrDm7G4-qN4KeZ4_5TyEilq2zOHHGFEzXpOq_UxqVnm3J4fhjqCNaS2nKd7\ HVVXGBQQ-4-FdVT-MyJXemw5maz2F_h324TQi6XoUPEwUddxBwLQFSOlzWnHYMc4_lcyZJ8MpTXCMPe_YJFNtb\ 9CaikKUdf8x4mzwH7usSf8s2d6R4dQITzKrjrEJ0u3w3eGkBBapoMVFBGPjP3Haz5FsVtHc5VEN3FZVIDF6Hrb\ JH1C4Q\",\ \"e\":\"AQAB\"\ }", public_key_jwk_str ); let message = "hello RSA".as_ref(); expect_rsa_sig( &private_key, message, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, "KBvV+F7Xofg4i4qUA0JEqfhQQdjZ7ralUYTPKRIitaKL4a6ni+abagsHs5V63+bmQF5t6DM4aRH2ZC943Tonkr\ AUY1mpaqic2vqtrtWk3cyrcHtkPCLNKzFf/6xvHPjeKH1Bu/qTQ0mn+hN6taOgw3ORbm6P9MkelX1RVEia98uwB\ Zn2BxKeqNYm11vqKDyS5ZFzHwpPrC4rri/uTIcXsQEXB+Lbb+naDpQn8qJqP+S+uM2LGWIXp5ExAJ55A111nIqE\ Ap0aKwf2U8Q81DWI8lbHbL1dd7FRDtZKm+ainO5ck4L/axtH7C4GIZd+TiXL3iYpiWmNkqlwv9WsNPe8Rg==", ); expect_rsa_sig( &private_key, message, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, "YsyhW9DkIoNJPqTNY7pidJi5wWtQGr4xety+2Zt1DKNMG0ENFkxCGPLCYcL9vGSS9kfkrPtQ3Eve7g9DKfg1fg\ 071SXJHxAlK0iC8mVYfQrxxyFlQDIPEhvCJx6VkWVm2jJhN+vByGRJLTo2n3gtYtMREfz+c5xnXWeIy+JQ0LXOy\ SyOZl0qHxn1VteczH79uCK0Vv8ZH3IfbQMU+2HjbVeUYRzCoAhlT4V2GY4U1pCrZBlfEyhr0ncHz90FRvvhLT3y\ SlHa7yY7CRJ+z1CLBOzBiH1Eko4tIJKy/qO9M6EGeFtXhqd4td5g2oY/mUZYjHYjgcDO+wAXrZ9lP/ZVUg==", ); expect_rsa_sig( &private_key, message, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, "GXN3rmZhlJw46FHoqiuELcIi6iUr3cVC0HZpjBJhrTVfta/8a4PpzmLbQxjdb1cdU/56XDXkqDSNvzRn4PwAOL\ 460n0Eg8d8mxwPRHQuyypze9240FEw3lyjp5uPJHn5PrmeelczJ1Xseinmp+JwpKHiHhmxp7FjgJc/o3J/hlz0n\ G1cgTndtrlp5JPJOJNt4XfgIgqoobH5Wk3ML35C50mD93Ld6V6nn6rK72wgecK1SDXeF4ztQUAjg4VTEojLm5VF\ kfR7kXV0dIbAvZXDa1uuIOlVDIRfF93rxme1Ze46Dywan+zfsGCcpFfFAsnGLsgNDmATB8IS1lTf1SGMoA==", ); expect_rsa_sig( &private_key, message, &CoreJwsSigningAlgorithm::RsaSsaPssSha256, "sPwTRDAOGOZr0ofAL/lMSXaR5L5aoFANLei4bqAQFHiFxDDrWZZ28MZiunwCGakOM8Itwas+sIX6vo3VTy9d5Bh\ Cotb/gY5DhMX/iZJubfS8U1fB8rFWXDpREeAegGvXPjBd0A4M6z9it9Tss43dYqO12mYEpz6rFvbHJKSR9Hnmak4\ F8TcuZswYtjhHhiib05PGjeJTo/5F15WtR7RYK4slYGOU4mf5wzZSpfgifJ2XjyQQe6oCFvVXftVtiQhEljHiEGv\ GZH5y4FA7h06PqkHMwQEnRsBhVm4pUqRi641tglZ3HZcoxYUE8AdMX7ltJzy/vpOew2bVGoF3mUnlZw==", ); expect_rsa_sig( &private_key, message, &CoreJwsSigningAlgorithm::RsaSsaPssSha384, "m0EsYFpNa5YjvEYPcfUpXPMqAWmWmkTaQiyK2HZ9Ejt+cBO/S5jcVqd0y2rCDMV1DpSb/JI8uhwp+qYm/2YKpIa\ zp+u9PpjlL3jvYn19WbwJTCztJ9XSjcEbtkf1fS/d/BU7FgQzYIE0k++QqHjgzkTI5+2XLYX2WP5dc0r67Or5xaF\ 0ixL1edpEDKfgF3jiKuLmR2dv4MWHPLYRb1I0zm5C/E7g57DfJT4uNzmLX9gTGr4xe6CxVEYy4eFdE+q1O5J6RXd\ FZnl4qFK9+x1pk0dhWkpIEaKhweI7YP79iFPnAiUnRM6BsdY+puwjGlaaGtYVFcuPO4uXEXtB1AnsEQ==", ); expect_rsa_sig( &private_key, message, &CoreJwsSigningAlgorithm::RsaSsaPssSha512, "N8BMNKm1dMOm0/BLzAjtnzvRlVtzgO8fUeKnfvUtK8XWeII5nk74hE3AoAJNPLuTninYtfaF68Supu5CsCJAqO9\ 1JnVvG8P1DX19iCTzJ83o69+kluBIz7x0l796RysDhqcjybGC+fj0M5MpgkNNcKlNwRixus3sfgCgh3mEB+E1Q11\ hQKjCTdyOcqzGoima+Na17VBWzU3XXLvB328UfkV2nswBlLUsZMT3I4n/aIziENQCLVPlLdX8z+1NjHSAgd9rZMf\ gfy0eMsjNuQpqPzVW3mbxlCMMVWpd8LKBprfa291xEk1wwvJCuU9EK7QmQPmYa1HAh+E+R2Dw3ibHdA==", ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::HmacSha256, message), Err(SigningError::UnsupportedAlg("HS256".to_string())), ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::HmacSha384, message), Err(SigningError::UnsupportedAlg("HS384".to_string())), ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::HmacSha512, message), Err(SigningError::UnsupportedAlg("HS512".to_string())), ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP256Sha256, message), Err(SigningError::UnsupportedAlg("ES256".to_string())), ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP384Sha384, message), Err(SigningError::UnsupportedAlg("ES384".to_string())), ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::EcdsaP521Sha512, message), Err(SigningError::UnsupportedAlg("ES512".to_string())), ); assert_eq!( private_key.sign(&CoreJwsSigningAlgorithm::None, message), Err(SigningError::UnsupportedAlg("none".to_string())), ); } #[test] fn test_rsa_pss_signing() { let private_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_KEY, None).unwrap(); const MESSAGE: &str = "This is a probabilistic signature scheme"; let sig1 = private_key .sign( &CoreJwsSigningAlgorithm::RsaSsaPssSha256, MESSAGE.as_bytes(), ) .unwrap(); let sig2 = private_key .sign( &CoreJwsSigningAlgorithm::RsaSsaPssSha256, MESSAGE.as_bytes(), ) .unwrap(); assert_ne!(sig1, sig2); } // Tests that JsonWebKeySet ignores unsupported keys during deserialization so that clients can // use providers that include unsupported keys as long as they only use supported ones to sign // payloads. #[test] fn test_jwks_unsupported_key() { let jwks_json = "{ \"keys\": [ { \"kty\": \"RSA\", \"use\": \"sig\", \"kid\": \"2011-04-29\", \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ jF44-csFCur-kEgU8awapJzKnqDKgw\", \"e\": \"AQAB\" }, { \"kty\": \"MAGIC\", \"use\": \"sig\", \"kid\": \"2040-01-01\", \"magic\": \"magic\" }, { \"kty\": \"EC\", \"use\": \"sig\", \"kid\": \"2011-05-01\", \"crv\": \"P-256\", \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" } ] }"; let jwks = serde_json::from_str::(jwks_json) .expect("deserialization should succeed"); assert_eq!(jwks.keys().len(), 2); assert_eq!(jwks.keys()[0].kty, CoreJsonWebKeyType::RSA); assert_eq!(jwks.keys()[0].use_, Some(CoreJsonWebKeyUse::Signature)); assert_eq!( jwks.keys()[0].kid, Some(JsonWebKeyId::new("2011-04-29".to_string())) ); assert_eq!(jwks.keys()[1].kty, CoreJsonWebKeyType::EllipticCurve); assert_eq!(jwks.keys()[1].use_, Some(CoreJsonWebKeyUse::Signature)); assert_eq!( jwks.keys()[1].kid, Some(JsonWebKeyId::new("2011-05-01".to_string())) ); assert_eq!(jwks.keys()[1].crv, Some(CoreJsonCurveType::P256)); } // Tests that JsonWebKeySet ignores keys with unsupported algorithms #[test] fn test_jwks_unsupported_alg() { let jwks_json = "{ \"keys\": [ { \"kty\": \"EC\", \"alg\": \"MAGIC\", \"crv\": \"P-256\", \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" }, { \"kty\": \"EC\", \"alg\": \"ES256\", \"kid\": \"2011-05-01\", \"crv\": \"P-256\", \"x\": \"kXCGZIr3oI6sKbnT6rRsIdxFXw3_VbLk_cveajgqXk8\", \"y\": \"StDvKIgXqAxJ6DuebREh-1vgvZRW3dfrOxSIKzBtRI0\" } ] }"; let jwks = serde_json::from_str::(jwks_json) .expect("deserialization should succeed"); assert_eq!(jwks.keys().len(), 1); let key = &jwks.keys()[0]; assert_eq!(&key.kid, &Some(JsonWebKeyId::new("2011-05-01".to_string()))); } // Test filtering keys by algorithm #[test] fn test_jwks_same_kid_different_alg() { let jwks_json = "{ \"keys\": [ { \"kty\": \"RSA\", \"use\": \"sig\", \"kid\": \"2011-04-29\", \"alg\": \"PS256\", \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ jF44-csFCur-kEgU8awapJzKnqDKgw\", \"e\": \"AQAB\" }, { \"kty\": \"RSA\", \"use\": \"sig\", \"kid\": \"2011-04-29\", \"alg\": \"PS384\", \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ jF44-csFCur-kEgU8awapJzKnqDKgw\", \"e\": \"AQAB\" } ] }"; let jwks = serde_json::from_str::(jwks_json) .expect("deserialization should succeed"); assert_eq!(jwks.keys().len(), 2); { let keys = jwks.filter_keys( Some(&JsonWebKeyId::new("2011-04-29".to_string())), &CoreJwsSigningAlgorithm::RsaSsaPssSha384, ); assert_eq!(keys.len(), 1); assert_eq!( keys[0].alg, Some(JsonWebTokenAlgorithm::Signature( CoreJwsSigningAlgorithm::RsaSsaPssSha384 )) ); } { let keys = jwks.filter_keys( Some(&JsonWebKeyId::new("2011-04-29".to_string())), &CoreJwsSigningAlgorithm::RsaSsaPssSha512, ); assert_eq!(keys.len(), 0); } } #[test] fn test_hash_bytes_eddsa() { let ed_key_json = "{ \"alg\": \"EdDSA\", \"crv\": \"Ed25519\", \"kty\": \"OKP\", \"use\": \"sig\", \"x\": \"vZ3CX884r0qNJ18pgXUTvFufK3ZmDzQfvMROJz6CLBc\" }"; let key: CoreJsonWebKey = serde_json::from_str(ed_key_json).expect("deserialization failed"); let hash = key .hash_bytes("justatest".as_bytes(), &CoreJwsSigningAlgorithm::EdDsa) .expect("Hashing should succeed"); assert_eq!( "2Kyc+bZQPseH8P3KAdKLu6D7stgxaNFXa/ckQX19RqZu9L65J0RmDlkULYCRExxRO77JW052i6r/+PK4rboICw==", BASE64_STANDARD.encode(hash) ); } #[test] fn test_hash_bytes_rsa() { let rsa_key_json = "{ \"kty\": \"RSA\", \"use\": \"sig\", \"kid\": \"2011-04-29\", \"n\": \"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhD\ R1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6C\ f0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1\ n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1\ jF44-csFCur-kEgU8awapJzKnqDKgw\", \"e\": \"AQAB\" }"; let key: CoreJsonWebKey = serde_json::from_str(rsa_key_json).expect("deserialization failed"); let hash = key .hash_bytes( "justatest".as_bytes(), &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, ) .expect("Hashing should succeed"); assert_eq!( "erGZHWfaQ5DcuMr6dSkOJDb/4VcxpPazY9vMsVB8dLo=", BASE64_STANDARD.encode(hash) ); let hash = key .hash_bytes( "justatest".as_bytes(), &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, ) .expect("Hashing should succeed"); assert_eq!( "JeQIV8/xLfKxSCCXuq7Hb/pIxnsjSZJM9+Dx23ah1oiEvra2q0Pm7eSS07gkl+Y2", BASE64_STANDARD.encode(hash) ); let hash = key .hash_bytes( "justatest".as_bytes(), &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, ) .expect("Hashing should succeed"); assert_eq!( "2Kyc+bZQPseH8P3KAdKLu6D7stgxaNFXa/ckQX19RqZu9L65J0RmDlkULYCRExxRO77JW052i6r/+PK4rboICw==", BASE64_STANDARD.encode(hash) ); } ================================================ FILE: src/core/mod.rs ================================================ use crate::registration::{ ClientMetadata, ClientRegistrationRequest, ClientRegistrationResponse, EmptyAdditionalClientMetadata, EmptyAdditionalClientRegistrationResponse, RegisterErrorResponseType, }; use crate::{ ApplicationType, AuthDisplay, AuthPrompt, AuthenticationFlow, ClaimName, ClaimType, Client, ClientAuthMethod, DeviceAuthorizationResponse, EmptyAdditionalClaims, EmptyAdditionalProviderMetadata, EmptyExtraDeviceAuthorizationFields, EmptyExtraTokenFields, ErrorResponseType, GenderClaim, GrantType, IdToken, IdTokenClaims, IdTokenFields, IdTokenVerifier, JsonWebKeySet, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, ProviderMetadata, ResponseMode, ResponseType, StandardErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse, SubjectIdentifierType, UserInfoClaims, UserInfoJsonWebToken, UserInfoVerifier, }; use base64::alphabet::URL_SAFE; use base64::engine::general_purpose::NO_PAD; use base64::engine::GeneralPurpose; use oauth2::{EndpointNotSet, ResponseType as OAuth2ResponseType}; use serde::{Deserialize, Serialize}; use std::fmt::Display; pub use crate::core::jwk::{ CoreEdDsaPrivateSigningKey, CoreHmacKey, CoreJsonCurveType, CoreJsonWebKey, CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreRsaPrivateSigningKey, }; pub use oauth2::basic::{ BasicErrorResponseType as CoreErrorResponseType, BasicRequestTokenError as CoreRequestTokenError, BasicRevocationErrorResponse as CoreRevocationErrorResponse, BasicTokenType as CoreTokenType, }; pub use oauth2::StandardRevocableToken as CoreRevocableToken; mod crypto; // Private purely for organizational reasons; exported publicly above. mod jwk; /// Standard implementation of DeviceAuthorizationResponse which throws away extra received response fields. pub type CoreDeviceAuthorizationResponse = DeviceAuthorizationResponse; /// OpenID Connect Core token introspection response. pub type CoreTokenIntrospectionResponse = StandardTokenIntrospectionResponse; /// OpenID Connect Core authentication flows. pub type CoreAuthenticationFlow = AuthenticationFlow; /// OpenID Connect Core client. pub type CoreClient< HasAuthUrl = EndpointNotSet, HasDeviceAuthUrl = EndpointNotSet, HasIntrospectionUrl = EndpointNotSet, HasRevocationUrl = EndpointNotSet, HasTokenUrl = EndpointNotSet, HasUserInfoUrl = EndpointNotSet, > = Client< EmptyAdditionalClaims, CoreAuthDisplay, CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJsonWebKey, CoreAuthPrompt, StandardErrorResponse, CoreTokenResponse, CoreTokenIntrospectionResponse, CoreRevocableToken, CoreRevocationErrorResponse, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, >; /// OpenID Connect Core client metadata. pub type CoreClientMetadata = ClientMetadata< EmptyAdditionalClientMetadata, CoreApplicationType, CoreClientAuthMethod, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseType, CoreSubjectIdentifierType, >; /// OpenID Connect Core client registration request. pub type CoreClientRegistrationRequest = ClientRegistrationRequest< EmptyAdditionalClientMetadata, EmptyAdditionalClientRegistrationResponse, CoreApplicationType, CoreClientAuthMethod, CoreRegisterErrorResponseType, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseType, CoreSubjectIdentifierType, >; /// OpenID Connect Core client registration response. pub type CoreClientRegistrationResponse = ClientRegistrationResponse< EmptyAdditionalClientMetadata, EmptyAdditionalClientRegistrationResponse, CoreApplicationType, CoreClientAuthMethod, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseType, CoreSubjectIdentifierType, >; /// OpenID Connect Core ID token. pub type CoreIdToken = IdToken< EmptyAdditionalClaims, CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, >; /// OpenID Connect Core ID token claims. pub type CoreIdTokenClaims = IdTokenClaims; /// OpenID Connect Core ID token fields. pub type CoreIdTokenFields = IdTokenFields< EmptyAdditionalClaims, EmptyExtraTokenFields, CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, >; /// OpenID Connect Core ID token verifier. pub type CoreIdTokenVerifier<'a> = IdTokenVerifier<'a, CoreJsonWebKey>; /// OpenID Connect Core token response. pub type CoreTokenResponse = StandardTokenResponse; /// OpenID Connect Core JSON Web Key Set. pub type CoreJsonWebKeySet = JsonWebKeySet; /// OpenID Connect Core provider metadata. pub type CoreProviderMetadata = ProviderMetadata< EmptyAdditionalProviderMetadata, CoreAuthDisplay, CoreClientAuthMethod, CoreClaimName, CoreClaimType, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, >; /// OpenID Connect Core user info claims. pub type CoreUserInfoClaims = UserInfoClaims; /// OpenID Connect Core user info JSON Web Token. pub type CoreUserInfoJsonWebToken = UserInfoJsonWebToken< EmptyAdditionalClaims, CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, >; /// OpenID Connect Core user info verifier. pub type CoreUserInfoVerifier<'a> = UserInfoVerifier<'a, CoreJweContentEncryptionAlgorithm, CoreJsonWebKey>; /// OpenID Connect Core client application type. /// /// These values are defined in /// [Section 2 of OpenID Connect Dynamic Client Registration 1.0]( /// http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata). #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreApplicationType { /// Native Clients MUST only register `redirect_uri`s using custom URI schemes or URLs using /// the `http` scheme with `localhost` as the hostname. Authorization Servers MAY place /// additional constraints on Native Clients. Native, /// Web Clients using the OAuth Implicit Grant Type MUST only register URLs using the `https` /// scheme as `redirect_uri`s; they MUST NOT use `localhost` as the hostname. Web, /// An extension not defined by the OpenID Connect Dynamic Client Registration spec. Extension(String), } // FIXME: Once https://github.com/serde-rs/serde/issues/912 is resolved, use #[serde(other)] instead // of custom serializer/deserializers. Right now this isn't possible because serde(other) only // supports unit variants. deserialize_from_str!(CoreApplicationType); serialize_as_str!(CoreApplicationType); impl CoreApplicationType { fn from_str(s: &str) -> Self { match s { "native" => CoreApplicationType::Native, "web" => CoreApplicationType::Web, ext => CoreApplicationType::Extension(ext.to_string()), } } } impl AsRef for CoreApplicationType { fn as_ref(&self) -> &str { match *self { CoreApplicationType::Native => "native", CoreApplicationType::Web => "web", CoreApplicationType::Extension(ref ext) => ext.as_str(), } } } impl ApplicationType for CoreApplicationType {} /// How the Authorization Server displays the authentication and consent user interface pages /// to the End-User. /// /// These values are defined in /// [Section 3.1.2.1](http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreAuthDisplay { /// The Authorization Server SHOULD display the authentication and consent UI consistent /// with a full User Agent page view. If the display parameter is not specified, this is /// the default display mode. Page, /// The Authorization Server SHOULD display the authentication and consent UI consistent /// with a popup User Agent window. The popup User Agent window should be of an appropriate /// size for a login-focused dialog and should not obscure the entire window that it is /// popping up over. Popup, /// The Authorization Server SHOULD display the authentication and consent UI consistent /// with a device that leverages a touch interface. Touch, /// The Authorization Server SHOULD display the authentication and consent UI consistent /// with a "feature phone" type display. Wap, /// An extension not defined by the OpenID Connect Core spec. Extension(String), } deserialize_from_str!(CoreAuthDisplay); serialize_as_str!(CoreAuthDisplay); impl CoreAuthDisplay { fn from_str(s: &str) -> Self { match s { "page" => CoreAuthDisplay::Page, "popup" => CoreAuthDisplay::Popup, "touch" => CoreAuthDisplay::Touch, "wap" => CoreAuthDisplay::Wap, ext => CoreAuthDisplay::Extension(ext.to_string()), } } } impl AsRef for CoreAuthDisplay { fn as_ref(&self) -> &str { match *self { CoreAuthDisplay::Page => "page", CoreAuthDisplay::Popup => "popup", CoreAuthDisplay::Touch => "touch", CoreAuthDisplay::Wap => "wap", CoreAuthDisplay::Extension(ref ext) => ext.as_str(), } } } impl AuthDisplay for CoreAuthDisplay {} impl Display for CoreAuthDisplay { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{}", self.as_ref()) } } /// Whether the Authorization Server should prompt the End-User for re-authentication and /// consent. /// /// These values are defined in /// [Section 3.1.2.1](http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreAuthPrompt { /// The Authorization Server MUST NOT display any authentication or consent user interface /// pages. An error is returned if an End-User is not already authenticated or the Client /// does not have pre-configured consent for the requested Claims or does not fulfill other /// conditions for processing the request. The error code will typically be /// `login_required,` `interaction_required`, or another code defined in /// [Section 3.1.2.6](http://openid.net/specs/openid-connect-core-1_0.html#AuthError). /// This can be used as a method to check for existing authentication and/or consent. None, /// The Authorization Server SHOULD prompt the End-User for reauthentication. If it cannot /// reauthenticate the End-User, it MUST return an error, typically `login_required`. Login, /// The Authorization Server SHOULD prompt the End-User for consent before returning /// information to the Client. If it cannot obtain consent, it MUST return an error, /// typically `consent_required`. Consent, /// The Authorization Server SHOULD prompt the End-User to select a user account. This /// enables an End-User who has multiple accounts at the Authorization Server to select /// amongst the multiple accounts that they might have current sessions for. If it cannot /// obtain an account selection choice made by the End-User, it MUST return an error, /// typically `account_selection_required`. SelectAccount, /// An extension not defined by the OpenID Connect Core spec. Extension(String), } deserialize_from_str!(CoreAuthPrompt); serialize_as_str!(CoreAuthPrompt); impl CoreAuthPrompt { fn from_str(s: &str) -> Self { match s { "none" => CoreAuthPrompt::None, "login" => CoreAuthPrompt::Login, "consent" => CoreAuthPrompt::Consent, "select_account" => CoreAuthPrompt::SelectAccount, ext => CoreAuthPrompt::Extension(ext.to_string()), } } } impl AsRef for CoreAuthPrompt { fn as_ref(&self) -> &str { match *self { CoreAuthPrompt::None => "none", CoreAuthPrompt::Login => "login", CoreAuthPrompt::Consent => "consent", CoreAuthPrompt::SelectAccount => "select_account", CoreAuthPrompt::Extension(ref ext) => ext.as_str(), } } } impl AuthPrompt for CoreAuthPrompt {} impl Display for CoreAuthPrompt { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{}", self.as_ref()) } } new_type![ /// OpenID Connect Core claim name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] CoreClaimName(String) ]; impl ClaimName for CoreClaimName {} /// Representation of a Claim Value. /// /// See [Section 5.6](http://openid.net/specs/openid-connect-core-1_0.html#ClaimTypes) for /// further information. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreClaimType { /// Aggregated Claim Type. /// /// See [Section 5.6.2]( /// http://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims) /// for details. Aggregated, /// Distributed Claim Type. /// /// See [Section 5.6.2]( /// http://openid.net/specs/openid-connect-core-1_0.html#AggregatedDistributedClaims) /// for details. Distributed, /// Normal Claims are represented as members in a JSON object. The Claim Name is the member /// name and the Claim Value is the member value. Normal, /// An extension not defined by the OpenID Connect Core spec. Extension(String), } deserialize_from_str!(CoreClaimType); serialize_as_str!(CoreClaimType); impl CoreClaimType { fn from_str(s: &str) -> Self { match s { "normal" => CoreClaimType::Normal, "aggregated" => CoreClaimType::Aggregated, "distributed" => CoreClaimType::Distributed, ext => CoreClaimType::Extension(ext.to_string()), } } } impl AsRef for CoreClaimType { fn as_ref(&self) -> &str { match *self { CoreClaimType::Normal => "normal", CoreClaimType::Aggregated => "aggregated", CoreClaimType::Distributed => "distributed", CoreClaimType::Extension(ref ext) => ext.as_str(), } } } impl ClaimType for CoreClaimType {} /// OpenID Connect Core client authentication method. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreClientAuthMethod { /// Client secret passed via the HTTP Basic authentication scheme. ClientSecretBasic, /// Client authentication using a JSON Web Token signed with the client secret used as an HMAC /// key. ClientSecretJwt, /// Client secret passed via the POST request body. ClientSecretPost, /// JSON Web Token signed with a private key whose public key has been previously registered /// with the OpenID Connect provider. PrivateKeyJwt, /// The Client does not authenticate itself at the Token Endpoint, either because it uses only /// the Implicit Flow (and so does not use the Token Endpoint) or because it is a Public Client /// with no Client Secret or other authentication mechanism. None, /// An extension not defined by the OpenID Connect Core spec. Extension(String), } deserialize_from_str!(CoreClientAuthMethod); serialize_as_str!(CoreClientAuthMethod); impl CoreClientAuthMethod { fn from_str(s: &str) -> Self { match s { "client_secret_basic" => CoreClientAuthMethod::ClientSecretBasic, "client_secret_jwt" => CoreClientAuthMethod::ClientSecretJwt, "client_secret_post" => CoreClientAuthMethod::ClientSecretPost, "private_key_jwt" => CoreClientAuthMethod::PrivateKeyJwt, "none" => CoreClientAuthMethod::None, ext => CoreClientAuthMethod::Extension(ext.to_string()), } } } impl AsRef for CoreClientAuthMethod { fn as_ref(&self) -> &str { match *self { CoreClientAuthMethod::ClientSecretBasic => "client_secret_basic", CoreClientAuthMethod::ClientSecretJwt => "client_secret_jwt", CoreClientAuthMethod::ClientSecretPost => "client_secret_post", CoreClientAuthMethod::PrivateKeyJwt => "private_key_jwt", CoreClientAuthMethod::None => "none", CoreClientAuthMethod::Extension(ref ext) => ext.as_str(), } } } impl ClientAuthMethod for CoreClientAuthMethod {} new_type![ /// OpenID Connect Core gender claim. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] CoreGenderClaim(String) ]; impl GenderClaim for CoreGenderClaim {} /// OpenID Connect Core grant type. /// // These are defined in various specs, including the Client Registration spec: // http://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum CoreGrantType { /// Authorization code grant. AuthorizationCode, /// Client credentials grant. ClientCredentials, /// Device Authorization Grant as described in [RFC 8628](https://tools.ietf.org/html/rfc8628). DeviceCode, /// Implicit grant. Implicit, /// JWT-based authentication as described in [RFC 7523](https://tools.ietf.org/html/rfc7523). JwtBearer, /// End user password grant. Password, /// Refresh token grant. RefreshToken, /// An extension not defined by any of the supported specifications. Extension(String), } deserialize_from_str!(CoreGrantType); serialize_as_str!(CoreGrantType); impl CoreGrantType { fn from_str(s: &str) -> Self { match s { "authorization_code" => CoreGrantType::AuthorizationCode, "client_credentials" => CoreGrantType::ClientCredentials, "urn:ietf:params:oauth:grant-type:device_code" => CoreGrantType::DeviceCode, "implicit" => CoreGrantType::Implicit, "urn:ietf:params:oauth:grant-type:jwt-bearer" => CoreGrantType::JwtBearer, "password" => CoreGrantType::Password, "refresh_token" => CoreGrantType::RefreshToken, ext => CoreGrantType::Extension(ext.to_string()), } } } impl AsRef for CoreGrantType { fn as_ref(&self) -> &str { match *self { CoreGrantType::AuthorizationCode => "authorization_code", CoreGrantType::ClientCredentials => "client_credentials", CoreGrantType::DeviceCode => "urn:ietf:params:oauth:grant-type:device_code", CoreGrantType::Implicit => "implicit", CoreGrantType::JwtBearer => "urn:ietf:params:oauth:grant-type:jwt-bearer", CoreGrantType::Password => "password", CoreGrantType::RefreshToken => "refresh_token", CoreGrantType::Extension(ref ext) => ext.as_str(), } } } impl GrantType for CoreGrantType {} /// OpenID Connect Core JWE encryption algorithms. /// /// These algorithms represent the `enc` header parameter values for JSON Web Encryption. /// The values are described in /// [Section 5.1 of RFC 7518](https://tools.ietf.org/html/rfc7518#section-5.1). #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[non_exhaustive] pub enum CoreJweContentEncryptionAlgorithm { /// AES-128 CBC HMAC SHA-256 authenticated encryption. #[serde(rename = "A128CBC-HS256")] Aes128CbcHmacSha256, /// AES-192 CBC HMAC SHA-384 authenticated encryption. #[serde(rename = "A192CBC-HS384")] Aes192CbcHmacSha384, /// AES-256 CBC HMAC SHA-512 authenticated encryption. #[serde(rename = "A256CBC-HS512")] Aes256CbcHmacSha512, /// AES-128 GCM. #[serde(rename = "A128GCM")] Aes128Gcm, /// AES-192 GCM. #[serde(rename = "A192GCM")] Aes192Gcm, /// AES-256 GCM. #[serde(rename = "A256GCM")] Aes256Gcm, } impl JweContentEncryptionAlgorithm for CoreJweContentEncryptionAlgorithm { type KeyType = CoreJsonWebKeyType; fn key_type(&self) -> Result { Ok(CoreJsonWebKeyType::Symmetric) } } /// OpenID Connect Core JWE key management algorithms. /// /// These algorithms represent the `alg` header parameter values for JSON Web Encryption. /// They are used to encrypt the Content Encryption Key (CEK) to produce the JWE Encrypted Key, or /// to use key agreement to agree upon the CEK. The values are described in /// [Section 4.1 of RFC 7518](https://tools.ietf.org/html/rfc7518#section-4.1). #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[non_exhaustive] pub enum CoreJweKeyManagementAlgorithm { /// RSAES-PKCS1-V1_5. #[serde(rename = "RSA1_5")] RsaPkcs1V15, /// RSAES OAEP using default parameters. #[serde(rename = "RSA-OAEP")] RsaOaep, /// RSAES OAEP using SHA-256 and MGF1 with SHA-256. #[serde(rename = "RSA-OAEP-256")] RsaOaepSha256, /// AES-128 Key Wrap. #[serde(rename = "A128KW")] AesKeyWrap128, /// AES-192 Key Wrap. #[serde(rename = "A192KW")] AesKeyWrap192, /// AES-256 Key Wrap. #[serde(rename = "A256KW")] AesKeyWrap256, /// Direct use of a shared symmetric key as the Content Encryption Key (CEK). #[serde(rename = "dir")] Direct, /// Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF. #[serde(rename = "ECDH-ES")] EcdhEs, /// ECDH-ES using Concat KDF and CEK wrapped with AES-128 Key Wrap. #[serde(rename = "ECDH-ES+A128KW")] EcdhEsAesKeyWrap128, /// ECDH-ES using Concat KDF and CEK wrapped with AES-192 Key Wrap. #[serde(rename = "ECDH-ES+A192KW")] EcdhEsAesKeyWrap192, /// ECDH-ES using Concat KDF and CEK wrapped with AES-256 Key Wrap. #[serde(rename = "ECDH-ES+A256KW")] EcdhEsAesKeyWrap256, /// Key wrapping with AES GCM using 128 bit key. #[serde(rename = "A128GCMKW")] Aes128Gcm, /// Key wrapping with AES GCM using 192 bit key. #[serde(rename = "A192GCMKW")] Aes192Gcm, /// Key wrapping with AES GCM using 256 bit key. #[serde(rename = "A256GCMKW")] Aes256Gcm, /// PBES2 with HMAC SHA-256 wrapped with AES-128 Key Wrap. #[serde(rename = "PBES2-HS256+A128KW")] PbEs2HmacSha256AesKeyWrap128, /// PBES2 with HMAC SHA-384 wrapped with AES-192 Key Wrap. #[serde(rename = "PBES2-HS384+A192KW")] PbEs2HmacSha384AesKeyWrap192, /// PBES2 with HMAC SHA-512 wrapped with AES-256 Key Wrap. #[serde(rename = "PBES2-HS512+A256KW")] PbEs2HmacSha512AesKeyWrap256, } impl JweKeyManagementAlgorithm for CoreJweKeyManagementAlgorithm {} /// OpenID Connect Core JWS signing algorithms. /// /// These algorithms represent the `alg` header parameter values for JSON Web Signature. /// They are used to digitally sign or create a MAC of the contents of the JWS Protected Header and /// the JWS Payload. The values are described in /// [Section 3.1 of RFC 7518](https://tools.ietf.org/html/rfc7518#section-3.1). #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[non_exhaustive] pub enum CoreJwsSigningAlgorithm { /// HMAC using SHA-256 (currently unsupported). #[serde(rename = "HS256")] HmacSha256, /// HMAC using SHA-384 (currently unsupported). #[serde(rename = "HS384")] HmacSha384, /// HMAC using SHA-512 (currently unsupported). #[serde(rename = "HS512")] HmacSha512, /// RSA SSA PKCS#1 v1.5 using SHA-256. #[serde(rename = "RS256")] RsaSsaPkcs1V15Sha256, /// RSA SSA PKCS#1 v1.5 using SHA-384. #[serde(rename = "RS384")] RsaSsaPkcs1V15Sha384, /// RSA SSA PKCS#1 v1.5 using SHA-512. #[serde(rename = "RS512")] RsaSsaPkcs1V15Sha512, /// ECDSA using P-256 and SHA-256 (currently unsupported). #[serde(rename = "ES256")] EcdsaP256Sha256, /// ECDSA using P-384 and SHA-384 (currently unsupported). #[serde(rename = "ES384")] EcdsaP384Sha384, /// ECDSA using P-521 and SHA-512 (currently unsupported). #[serde(rename = "ES512")] EcdsaP521Sha512, /// RSA SSA-PSS using SHA-256 and MGF1 with SHA-256. #[serde(rename = "PS256")] RsaSsaPssSha256, /// RSA SSA-PSS using SHA-384 and MGF1 with SHA-384. #[serde(rename = "PS384")] RsaSsaPssSha384, /// RSA SSA-PSS using SHA-512 and MGF1 with SHA-512. #[serde(rename = "PS512")] RsaSsaPssSha512, /// EdDSA signature (algorithm depends on `crv` header). #[serde(rename = "EdDSA")] EdDsa, /// No digital signature or MAC performed. /// /// # Security Warning /// /// This algorithm provides no security over the integrity of the JSON Web Token. Clients /// should be careful not to rely on unsigned JWT's for security purposes. See /// [Critical vulnerabilities in JSON Web Token libraries]( /// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) for /// further discussion. #[serde(rename = "none")] None, } impl JwsSigningAlgorithm for CoreJwsSigningAlgorithm { type KeyType = CoreJsonWebKeyType; fn key_type(&self) -> Option { match *self { CoreJwsSigningAlgorithm::HmacSha256 | CoreJwsSigningAlgorithm::HmacSha384 | CoreJwsSigningAlgorithm::HmacSha512 => Some(CoreJsonWebKeyType::Symmetric), CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 | CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 | CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512 | CoreJwsSigningAlgorithm::RsaSsaPssSha256 | CoreJwsSigningAlgorithm::RsaSsaPssSha384 | CoreJwsSigningAlgorithm::RsaSsaPssSha512 => Some(CoreJsonWebKeyType::RSA), CoreJwsSigningAlgorithm::EcdsaP256Sha256 | CoreJwsSigningAlgorithm::EcdsaP384Sha384 | CoreJwsSigningAlgorithm::EcdsaP521Sha512 => Some(CoreJsonWebKeyType::EllipticCurve), CoreJwsSigningAlgorithm::EdDsa => Some(CoreJsonWebKeyType::OctetKeyPair), CoreJwsSigningAlgorithm::None => None, } } fn uses_shared_secret(&self) -> bool { self.key_type() .map(|kty| kty == CoreJsonWebKeyType::Symmetric) .unwrap_or(false) } fn rsa_sha_256() -> Self { CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 } } /// OpenID Connect Core authentication error response types. /// /// This type represents errors returned in a redirect from the Authorization Endpoint to the /// client's redirect URI. /// /// These values are defined across both /// [Section 4.1.2.1](https://tools.ietf.org/html/rfc6749#section-4.1.2.1) of RFC 6749 and /// [Section 3.1.2.6](https://openid.net/specs/openid-connect-core-1_0.html#AuthError) of the /// OpenID Connect Core spec. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreAuthErrorResponseType { /// The resource owner or authorization server denied the request. AccessDenied, /// The End-User is REQUIRED to select a session at the Authorization Server. The End-User MAY /// be authenticated at the Authorization Server with different associated accounts, but the /// End-User did not select a session. This error MAY be returned when the `prompt` parameter /// value in the Authentication Request is `none`, but the Authentication Request cannot be /// completed without displaying a user interface to prompt for a session to use. AccountSelectionRequired, /// The Authorization Server requires End-User consent. This error MAY be returned when the /// `prompt` parameter value in the Authentication Request is `none`, but the Authentication /// Request cannot be completed without displaying a user interface for End-User consent. ConsentRequired, /// The Authorization Server requires End-User interaction of some form to proceed. This error /// MAY be returned when the `prompt` parameter value in the Authentication Request is `none`, /// but the Authentication Request cannot be completed without displaying a user interface for /// End-User interaction. InteractionRequired, /// The request is missing a required parameter, includes an invalid parameter value, includes /// a parameter more than once, or is otherwise malformed. InvalidRequest, /// The `request` parameter contains an invalid Request Object. InvalidRequestObject, /// The `request_uri` in the Authorization Request returns an error or contains invalid data. InvalidRequestUri, /// The requested scope is invalid, unknown, or malformed. InvalidScope, /// The Authorization Server requires End-User authentication. This error MAY be returned when /// the `prompt` parameter value in the Authentication Request is `none`, but the Authentication /// Request cannot be completed without displaying a user interface for End-User authentication. LoginRequired, /// The OpenID Connect Provider does not support use of the `registration` parameter. RegistrationNotSupported, /// The OpenID Connect Provider does not support use of the `request` parameter. RequestNotSupported, /// The OpenID Connect Provider does not support use of the `request_uri` parameter. RequestUriNotSupported, /// The authorization server encountered an unexpected condition that prevented it from /// fulfilling the request. (This error code is needed because a 500 Internal Server Error HTTP /// status code cannot be returned to the client via an HTTP redirect.) ServerError, /// The authorization server is currently unable to handle the request due to a temporary /// overloading or maintenance of the server. (This error code is needed because a 503 Service /// Unavailable HTTP status code cannot be returned to the client via an HTTP redirect.) TemporarilyUnavailable, /// The client is not authorized to request an authorization code using this method. UnauthorizedClient, /// The authorization server does not support obtaining an authorization code using this method. UnsupportedResponseType, /// An extension not defined by any of the supported specifications. Extension(String), } deserialize_from_str!(CoreAuthErrorResponseType); serialize_as_str!(CoreAuthErrorResponseType); impl CoreAuthErrorResponseType { fn from_str(s: &str) -> Self { match s { "access_denied" => CoreAuthErrorResponseType::AccessDenied, "account_selection_required" => CoreAuthErrorResponseType::AccountSelectionRequired, "consent_required" => CoreAuthErrorResponseType::ConsentRequired, "interaction_required" => CoreAuthErrorResponseType::InteractionRequired, "invalid_request" => CoreAuthErrorResponseType::InvalidRequest, "invalid_request_object" => CoreAuthErrorResponseType::InvalidRequestObject, "invalid_request_uri" => CoreAuthErrorResponseType::InvalidRequestUri, "invalid_scope" => CoreAuthErrorResponseType::InvalidScope, "login_required" => CoreAuthErrorResponseType::LoginRequired, "registration_not_supported" => CoreAuthErrorResponseType::RegistrationNotSupported, "request_not_supported" => CoreAuthErrorResponseType::RequestNotSupported, "request_uri_not_supported" => CoreAuthErrorResponseType::RequestUriNotSupported, "server_error" => CoreAuthErrorResponseType::ServerError, "temporarily_unavailable" => CoreAuthErrorResponseType::TemporarilyUnavailable, "unauthorized_client" => CoreAuthErrorResponseType::UnauthorizedClient, "unsupported_response_type" => CoreAuthErrorResponseType::UnsupportedResponseType, ext => CoreAuthErrorResponseType::Extension(ext.to_string()), } } } impl AsRef for CoreAuthErrorResponseType { fn as_ref(&self) -> &str { match *self { CoreAuthErrorResponseType::AccessDenied => "access_denied", CoreAuthErrorResponseType::AccountSelectionRequired => "account_selection_required", CoreAuthErrorResponseType::ConsentRequired => "consent_required", CoreAuthErrorResponseType::InteractionRequired => "interaction_required", CoreAuthErrorResponseType::InvalidRequest => "invalid_request", CoreAuthErrorResponseType::InvalidRequestObject => "invalid_request_obbject", CoreAuthErrorResponseType::InvalidRequestUri => "invalid_request_uri", CoreAuthErrorResponseType::InvalidScope => "invalid_scope", CoreAuthErrorResponseType::LoginRequired => "login_required", CoreAuthErrorResponseType::RegistrationNotSupported => "registration_not_supported", CoreAuthErrorResponseType::RequestNotSupported => "request_not_supported", CoreAuthErrorResponseType::RequestUriNotSupported => "request_uri_not_supported", CoreAuthErrorResponseType::ServerError => "server_error", CoreAuthErrorResponseType::TemporarilyUnavailable => "temporarily_unavailable", CoreAuthErrorResponseType::UnauthorizedClient => "unauthorized_client", CoreAuthErrorResponseType::UnsupportedResponseType => "unsupported_response_type", CoreAuthErrorResponseType::Extension(ref ext) => ext.as_str(), } } } /// OpenID Connect Core registration error response type. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreRegisterErrorResponseType { /// The value of one of the Client Metadata fields is invalid and the server has rejected this /// request. Note that an Authorization Server MAY choose to substitute a valid value for any /// requested parameter of a Client's Metadata. InvalidClientMetadata, /// The value of one or more `redirect_uri`s is invalid. InvalidRedirectUri, /// An extension not defined by any of the supported specifications. Extension(String), } deserialize_from_str!(CoreRegisterErrorResponseType); serialize_as_str!(CoreRegisterErrorResponseType); impl CoreRegisterErrorResponseType { fn from_str(s: &str) -> Self { match s { "invalid_client_metadata" => CoreRegisterErrorResponseType::InvalidClientMetadata, "invalid_redirect_uri" => CoreRegisterErrorResponseType::InvalidRedirectUri, ext => CoreRegisterErrorResponseType::Extension(ext.to_string()), } } } impl AsRef for CoreRegisterErrorResponseType { fn as_ref(&self) -> &str { match *self { CoreRegisterErrorResponseType::InvalidClientMetadata => "invalid_client_metadata", CoreRegisterErrorResponseType::InvalidRedirectUri => "invalid_redirect_uri", CoreRegisterErrorResponseType::Extension(ref ext) => ext.as_str(), } } } impl ErrorResponseType for CoreRegisterErrorResponseType {} impl RegisterErrorResponseType for CoreRegisterErrorResponseType {} impl Display for CoreRegisterErrorResponseType { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{}", self.as_ref()) } } /// OpenID Connect Core response mode. /// /// Informs the Authorization Server of the mechanism to be used for returning Authorization /// Response parameters from the Authorization Endpoint. /// /// The default Response Mode for the OAuth 2.0 `code` Response Type is the `query` encoding. /// The default Response Mode for the OAuth 2.0 `token` Response Type is the `fragment` encoding. /// These values are defined in /// [OAuth 2.0 Multiple Response Type Encoding Practices]( /// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes) /// and [OAuth 2.0 Form Post Response Mode]( /// http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode). #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreResponseMode { /// In this mode, Authorization Response parameters are encoded in the query string added to /// the `redirect_uri` when redirecting back to the Client. Query, /// In this mode, Authorization Response parameters are encoded in the fragment added to the /// `redirect_uri` when redirecting back to the Client. Fragment, /// In this mode, Authorization Response parameters are encoded as HTML form values that are /// auto-submitted in the User Agent, and thus are transmitted via the HTTP `POST` method to the /// Client, with the result parameters being encoded in the body using the /// `application/x-www-form-urlencoded` format. The `action` attribute of the form MUST be the /// Client's Redirection URI. The method of the form attribute MUST be `POST`. Because the /// Authorization Response is intended to be used only once, the Authorization Server MUST /// instruct the User Agent (and any intermediaries) not to store or reuse the content of the /// response. /// /// Any technique supported by the User Agent MAY be used to cause the submission of the form, /// and any form content necessary to support this MAY be included, such as submit controls and /// client-side scripting commands. However, the Client MUST be able to process the message /// without regard for the mechanism by which the form submission was initiated. /// /// See [OAuth 2.0 Form Post Response Mode]( /// http://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode) /// for further information. FormPost, /// An extension not defined by any of the supported specifications. Extension(String), } deserialize_from_str!(CoreResponseMode); serialize_as_str!(CoreResponseMode); impl CoreResponseMode { fn from_str(s: &str) -> Self { match s { "query" => CoreResponseMode::Query, "fragment" => CoreResponseMode::Fragment, "form_post" => CoreResponseMode::FormPost, ext => CoreResponseMode::Extension(ext.to_string()), } } } impl AsRef for CoreResponseMode { fn as_ref(&self) -> &str { match *self { CoreResponseMode::Query => "query", CoreResponseMode::Fragment => "fragment", CoreResponseMode::FormPost => "form_post", CoreResponseMode::Extension(ref ext) => ext.as_str(), } } } impl ResponseMode for CoreResponseMode {} /// OpenID Connect Core response type. /// /// Informs the Authorization Server of the desired authorization processing flow, including what /// parameters are returned from the endpoints used. /// /// This type represents a single Response Type. Multiple Response Types are represented via the /// `ResponseTypes` type, which wraps a `Vec`. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreResponseType { /// Used by the OAuth 2.0 Authorization Code Flow. Code, /// When supplied as the `response_type` parameter in an OAuth 2.0 Authorization Request, a /// successful response MUST include the parameter `id_token`. IdToken, /// When supplied as the `response_type` parameter in an OAuth 2.0 Authorization Request, the /// Authorization Server SHOULD NOT return an OAuth 2.0 Authorization Code, Access Token, Access /// Token Type, or ID Token in a successful response to the grant request. If a `redirect_uri` /// is supplied, the User Agent SHOULD be redirected there after granting or denying access. /// The request MAY include a `state` parameter, and if so, the Authorization Server MUST echo /// its value as a response parameter when issuing either a successful response or an error /// response. The default Response Mode for this Response Type is the query encoding. Both /// successful and error responses SHOULD be returned using the supplied Response Mode, or if /// none is supplied, using the default Response Mode. /// /// This Response Type is not generally used with OpenID Connect but may be supported by the /// Authorization Server. None, /// Used by the OAuth 2.0 Implicit Flow. Token, /// An extension not defined by the OpenID Connect Core spec. Extension(String), } deserialize_from_str!(CoreResponseType); serialize_as_str!(CoreResponseType); impl CoreResponseType { fn from_str(s: &str) -> Self { match s { "code" => CoreResponseType::Code, "id_token" => CoreResponseType::IdToken, "none" => CoreResponseType::None, "token" => CoreResponseType::Token, ext => CoreResponseType::Extension(ext.to_string()), } } } impl AsRef for CoreResponseType { fn as_ref(&self) -> &str { match *self { CoreResponseType::Code => "code", CoreResponseType::IdToken => "id_token", CoreResponseType::None => "none", CoreResponseType::Token => "token", CoreResponseType::Extension(ref ext) => ext.as_str(), } } } impl ResponseType for CoreResponseType { fn to_oauth2(&self) -> OAuth2ResponseType { OAuth2ResponseType::new(self.as_ref().to_string()) } } /// OpenID Connect Core Subject Identifier type. /// /// A Subject Identifier is a locally unique and never reassigned identifier within the Issuer for /// the End-User, which is intended to be consumed by the Client. /// /// See [Section 8](http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes) for /// further information. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum CoreSubjectIdentifierType { /// This provides a different `sub` value to each Client, so as not to enable Clients to /// correlate the End-User's activities without permission. Pairwise, /// This provides the same `sub` (subject) value to all Clients. It is the default if the /// provider has no `subject_types_supported` element in its discovery document. Public, /// An extension not defined by the OpenID Connect Core spec. Extension(String), } deserialize_from_str!(CoreSubjectIdentifierType); serialize_as_str!(CoreSubjectIdentifierType); impl CoreSubjectIdentifierType { fn from_str(s: &str) -> Self { match s { "pairwise" => CoreSubjectIdentifierType::Pairwise, "public" => CoreSubjectIdentifierType::Public, ext => CoreSubjectIdentifierType::Extension(ext.to_string()), } } } impl AsRef for CoreSubjectIdentifierType { fn as_ref(&self) -> &str { match *self { CoreSubjectIdentifierType::Pairwise => "pairwise", CoreSubjectIdentifierType::Public => "public", CoreSubjectIdentifierType::Extension(ref ext) => ext.as_str(), } } } impl SubjectIdentifierType for CoreSubjectIdentifierType {} pub(crate) fn base64_url_safe_no_pad() -> GeneralPurpose { GeneralPurpose::new(&URL_SAFE, NO_PAD.with_decode_allow_trailing_bits(true)) } #[cfg(test)] mod tests; ================================================ FILE: src/core/tests.rs ================================================ use crate::core::{CoreGrantType, CoreJwsSigningAlgorithm}; #[test] fn test_grant_type_serialize() { let serialized_implicit = serde_json::to_string(&CoreGrantType::Implicit).unwrap(); assert_eq!("\"implicit\"", serialized_implicit); assert_eq!( CoreGrantType::Implicit, serde_json::from_str::(&serialized_implicit).unwrap() ); } #[test] fn test_signature_alg_serde_plain() { assert_eq!( serde_plain::to_string(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256).unwrap(), "RS256" ); assert_eq!( serde_plain::from_str::("RS256").unwrap(), CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 ); } ================================================ FILE: src/discovery/mod.rs ================================================ use crate::http_utils::{check_content_type, MIME_TYPE_JSON}; use crate::{ AsyncHttpClient, AuthDisplay, AuthUrl, AuthenticationContextClass, ClaimName, ClaimType, ClientAuthMethod, GrantType, HttpRequest, HttpResponse, IssuerUrl, JsonWebKey, JsonWebKeySet, JsonWebKeySetUrl, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, LanguageTag, OpPolicyUrl, OpTosUrl, RegistrationUrl, ResponseMode, ResponseType, ResponseTypes, Scope, ServiceDocUrl, SubjectIdentifierType, SyncHttpClient, TokenUrl, UserInfoUrl, }; use http::header::{HeaderValue, ACCEPT}; use http::method::Method; use http::status::StatusCode; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, skip_serializing_none, VecSkipError}; use thiserror::Error; use std::fmt::Debug; use std::future::Future; #[cfg(test)] mod tests; const CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration"; /// Trait for adding extra fields to [`ProviderMetadata`]. pub trait AdditionalProviderMetadata: Clone + Debug + DeserializeOwned + Serialize {} // In order to support serde flatten, this must be an empty struct rather than an empty // tuple struct. /// Empty (default) extra [`ProviderMetadata`] fields. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct EmptyAdditionalProviderMetadata {} impl AdditionalProviderMetadata for EmptyAdditionalProviderMetadata {} /// Provider metadata returned by [OpenID Connect Discovery]( /// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). #[serde_as] #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[allow(clippy::type_complexity)] pub struct ProviderMetadata where A: AdditionalProviderMetadata, AD: AuthDisplay, CA: ClientAuthMethod, CN: ClaimName, CT: ClaimType, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RM: ResponseMode, RT: ResponseType, S: SubjectIdentifierType, { issuer: IssuerUrl, authorization_endpoint: AuthUrl, token_endpoint: Option, userinfo_endpoint: Option, jwks_uri: JsonWebKeySetUrl, #[serde(default = "JsonWebKeySet::default", skip)] jwks: JsonWebKeySet, registration_endpoint: Option, scopes_supported: Option>, #[serde(bound(deserialize = "RT: ResponseType"))] response_types_supported: Vec>, #[serde(bound(deserialize = "RM: ResponseMode"))] response_modes_supported: Option>, #[serde(bound(deserialize = "G: GrantType"))] grant_types_supported: Option>, acr_values_supported: Option>, #[serde(bound(deserialize = "S: SubjectIdentifierType"))] subject_types_supported: Vec, #[serde(bound(deserialize = "K: JsonWebKey"))] #[serde_as(as = "VecSkipError<_>")] id_token_signing_alg_values_supported: Vec, #[serde( bound(deserialize = "JK: JweKeyManagementAlgorithm"), default = "Option::default" )] #[serde_as(as = "Option>")] id_token_encryption_alg_values_supported: Option>, #[serde( bound( deserialize = "JE: JweContentEncryptionAlgorithm::KeyType>" ), default = "Option::default" )] #[serde_as(as = "Option>")] id_token_encryption_enc_values_supported: Option>, #[serde(bound(deserialize = "K: JsonWebKey"), default = "Option::default")] #[serde_as(as = "Option>")] userinfo_signing_alg_values_supported: Option>, #[serde( bound(deserialize = "JK: JweKeyManagementAlgorithm"), default = "Option::default" )] #[serde_as(as = "Option>")] userinfo_encryption_alg_values_supported: Option>, #[serde( bound( deserialize = "JE: JweContentEncryptionAlgorithm::KeyType>" ), default = "Option::default" )] #[serde_as(as = "Option>")] userinfo_encryption_enc_values_supported: Option>, #[serde(bound(deserialize = "K: JsonWebKey"), default = "Option::default")] #[serde_as(as = "Option>")] request_object_signing_alg_values_supported: Option>, #[serde( bound(deserialize = "JK: JweKeyManagementAlgorithm"), default = "Option::default" )] #[serde_as(as = "Option>")] request_object_encryption_alg_values_supported: Option>, #[serde( bound( deserialize = "JE: JweContentEncryptionAlgorithm::KeyType>" ), default = "Option::default" )] #[serde_as(as = "Option>")] request_object_encryption_enc_values_supported: Option>, #[serde(bound(deserialize = "CA: ClientAuthMethod"))] token_endpoint_auth_methods_supported: Option>, #[serde(bound(deserialize = "K: JsonWebKey"), default = "Option::default")] #[serde_as(as = "Option>")] token_endpoint_auth_signing_alg_values_supported: Option>, #[serde(bound(deserialize = "AD: AuthDisplay"))] display_values_supported: Option>, #[serde(bound(deserialize = "CT: ClaimType"))] claim_types_supported: Option>, #[serde(bound(deserialize = "CN: ClaimName"))] claims_supported: Option>, service_documentation: Option, claims_locales_supported: Option>, ui_locales_supported: Option>, claims_parameter_supported: Option, request_parameter_supported: Option, request_uri_parameter_supported: Option, require_request_uri_registration: Option, op_policy_uri: Option, op_tos_uri: Option, #[serde(bound(deserialize = "A: AdditionalProviderMetadata"), flatten)] additional_metadata: A, } impl ProviderMetadata where A: AdditionalProviderMetadata, AD: AuthDisplay, CA: ClientAuthMethod, CN: ClaimName, CT: ClaimType, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RM: ResponseMode, RT: ResponseType, S: SubjectIdentifierType, { /// Instantiates new provider metadata. pub fn new( issuer: IssuerUrl, authorization_endpoint: AuthUrl, jwks_uri: JsonWebKeySetUrl, response_types_supported: Vec>, subject_types_supported: Vec, id_token_signing_alg_values_supported: Vec, additional_metadata: A, ) -> Self { Self { issuer, authorization_endpoint, token_endpoint: None, userinfo_endpoint: None, jwks_uri, jwks: JsonWebKeySet::new(Vec::new()), registration_endpoint: None, scopes_supported: None, response_types_supported, response_modes_supported: None, grant_types_supported: None, acr_values_supported: None, subject_types_supported, id_token_signing_alg_values_supported, id_token_encryption_alg_values_supported: None, id_token_encryption_enc_values_supported: None, userinfo_signing_alg_values_supported: None, userinfo_encryption_alg_values_supported: None, userinfo_encryption_enc_values_supported: None, request_object_signing_alg_values_supported: None, request_object_encryption_alg_values_supported: None, request_object_encryption_enc_values_supported: None, token_endpoint_auth_methods_supported: None, token_endpoint_auth_signing_alg_values_supported: None, display_values_supported: None, claim_types_supported: None, claims_supported: None, service_documentation: None, claims_locales_supported: None, ui_locales_supported: None, claims_parameter_supported: None, request_parameter_supported: None, request_uri_parameter_supported: None, require_request_uri_registration: None, op_policy_uri: None, op_tos_uri: None, additional_metadata, } } field_getters_setters![ pub self [self] ["provider metadata value"] { set_issuer -> issuer[IssuerUrl], set_authorization_endpoint -> authorization_endpoint[AuthUrl], set_token_endpoint -> token_endpoint[Option], set_userinfo_endpoint -> userinfo_endpoint[Option], set_jwks_uri -> jwks_uri[JsonWebKeySetUrl], set_jwks -> jwks[JsonWebKeySet], set_registration_endpoint -> registration_endpoint[Option], set_scopes_supported -> scopes_supported[Option>], set_response_types_supported -> response_types_supported[Vec>], set_response_modes_supported -> response_modes_supported[Option>], set_grant_types_supported -> grant_types_supported[Option>], set_acr_values_supported -> acr_values_supported[Option>], set_subject_types_supported -> subject_types_supported[Vec], set_id_token_signing_alg_values_supported -> id_token_signing_alg_values_supported[Vec], set_id_token_encryption_alg_values_supported -> id_token_encryption_alg_values_supported[Option>], set_id_token_encryption_enc_values_supported -> id_token_encryption_enc_values_supported[Option>], set_userinfo_signing_alg_values_supported -> userinfo_signing_alg_values_supported[Option>], set_userinfo_encryption_alg_values_supported -> userinfo_encryption_alg_values_supported[Option>], set_userinfo_encryption_enc_values_supported -> userinfo_encryption_enc_values_supported[Option>], set_request_object_signing_alg_values_supported -> request_object_signing_alg_values_supported[Option>], set_request_object_encryption_alg_values_supported -> request_object_encryption_alg_values_supported[Option>], set_request_object_encryption_enc_values_supported -> request_object_encryption_enc_values_supported[Option>], set_token_endpoint_auth_methods_supported -> token_endpoint_auth_methods_supported[Option>], set_token_endpoint_auth_signing_alg_values_supported -> token_endpoint_auth_signing_alg_values_supported[Option>], set_display_values_supported -> display_values_supported[Option>], set_claim_types_supported -> claim_types_supported[Option>], set_claims_supported -> claims_supported[Option>], set_service_documentation -> service_documentation[Option], set_claims_locales_supported -> claims_locales_supported[Option>], set_ui_locales_supported -> ui_locales_supported[Option>], set_claims_parameter_supported -> claims_parameter_supported[Option], set_request_parameter_supported -> request_parameter_supported[Option], set_request_uri_parameter_supported -> request_uri_parameter_supported[Option], set_require_request_uri_registration -> require_request_uri_registration[Option], set_op_policy_uri -> op_policy_uri[Option], set_op_tos_uri -> op_tos_uri[Option], } ]; /// Fetches the OpenID Connect Discovery document and associated JSON Web Key Set from the /// OpenID Connect Provider. pub fn discover( issuer_url: &IssuerUrl, http_client: &C, ) -> Result::Error>> where C: SyncHttpClient, { let discovery_url = issuer_url .join(CONFIG_URL_SUFFIX) .map_err(DiscoveryError::UrlParse)?; http_client .call( Self::discovery_request(discovery_url.clone()).map_err(|err| { DiscoveryError::Other(format!("failed to prepare request: {err}")) })?, ) .map_err(DiscoveryError::Request) .and_then(|http_response| { Self::discovery_response(issuer_url, &discovery_url, http_response) }) .and_then(|provider_metadata| { JsonWebKeySet::fetch(provider_metadata.jwks_uri(), http_client).map(|jwks| Self { jwks, ..provider_metadata }) }) } /// Asynchronously fetches the OpenID Connect Discovery document and associated JSON Web Key Set /// from the OpenID Connect Provider. pub fn discover_async<'c, C>( issuer_url: IssuerUrl, http_client: &'c C, ) -> impl Future>::Error>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { let discovery_url = issuer_url .join(CONFIG_URL_SUFFIX) .map_err(DiscoveryError::UrlParse)?; let provider_metadata = http_client .call( Self::discovery_request(discovery_url.clone()).map_err(|err| { DiscoveryError::Other(format!("failed to prepare request: {err}")) })?, ) .await .map_err(DiscoveryError::Request) .and_then(|http_response| { Self::discovery_response(&issuer_url, &discovery_url, http_response) })?; JsonWebKeySet::fetch_async(provider_metadata.jwks_uri(), http_client) .await .map(|jwks| Self { jwks, ..provider_metadata }) }) } fn discovery_request(discovery_url: url::Url) -> Result { http::Request::builder() .uri(discovery_url.to_string()) .method(Method::GET) .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) .body(Vec::new()) } fn discovery_response( issuer_url: &IssuerUrl, discovery_url: &url::Url, discovery_response: HttpResponse, ) -> Result> where RE: std::error::Error + 'static, { if discovery_response.status() != StatusCode::OK { return Err(DiscoveryError::Response( discovery_response.status(), discovery_response.body().to_owned(), format!( "HTTP status code {} at {}", discovery_response.status(), discovery_url ), )); } check_content_type(discovery_response.headers(), MIME_TYPE_JSON).map_err(|err_msg| { DiscoveryError::Response( discovery_response.status(), discovery_response.body().to_owned(), err_msg, ) })?; let provider_metadata = serde_path_to_error::deserialize::<_, Self>( &mut serde_json::Deserializer::from_slice(discovery_response.body()), ) .map_err(DiscoveryError::Parse)?; if provider_metadata.issuer() != issuer_url { Err(DiscoveryError::Validation(format!( "unexpected issuer URI `{}` (expected `{}`)", provider_metadata.issuer().as_str(), issuer_url.as_str() ))) } else { Ok(provider_metadata) } } /// Returns additional provider metadata fields. pub fn additional_metadata(&self) -> &A { &self.additional_metadata } /// Returns mutable additional provider metadata fields. pub fn additional_metadata_mut(&mut self) -> &mut A { &mut self.additional_metadata } } /// Error retrieving provider metadata. #[derive(Debug, Error)] #[non_exhaustive] pub enum DiscoveryError where RE: std::error::Error + 'static, { /// An unexpected error occurred. #[error("Other error: {0}")] Other(String), /// Failed to parse server response. #[error("Failed to parse server response")] Parse(#[source] serde_path_to_error::Error), /// An error occurred while sending the request or receiving the response (e.g., network /// connectivity failed). #[error("Request failed")] Request(#[source] RE), /// Server returned an invalid response. #[error("Server returned invalid response: {2}")] Response(StatusCode, Vec, String), /// Failed to parse discovery URL from issuer URL. #[error("Failed to parse URL")] UrlParse(#[source] url::ParseError), /// Failed to validate provider metadata. #[error("Validation error: {0}")] Validation(String), } ================================================ FILE: src/discovery/tests.rs ================================================ use crate::core::{ CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, }; use crate::{ AuthUrl, AuthenticationContextClass, IssuerUrl, JsonWebKeySetUrl, LanguageTag, OpPolicyUrl, OpTosUrl, RegistrationUrl, ResponseTypes, Scope, ServiceDocUrl, TokenUrl, UserInfoUrl, }; #[test] fn test_discovery_deserialization() { // Fetched from: https://rp.certification.openid.net:8080/openidconnect-rs/ // rp-response_type-code/.well-known/openid-configuration let json_response_standard = "\ \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ \"token_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/token\",\ \"userinfo_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/userinfo\",\ \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ \"registration_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/registration\",\ \"scopes_supported\":[\ \"email\",\ \"phone\",\ \"profile\",\ \"openid\",\ \"address\",\ \"offline_access\",\ \"openid\"\ ],\ \"response_types_supported\":[\ \"code\"\ ],\ \"response_modes_supported\":[\ \"query\",\ \"fragment\",\ \"form_post\"\ ],\ \"grant_types_supported\":[\ \"authorization_code\",\ \"implicit\",\ \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\ \"refresh_token\"\ ],\ \"acr_values_supported\":[\ \"PASSWORD\"\ ],\ \"subject_types_supported\":[\ \"public\",\ \"pairwise\"\ ],\ \"id_token_signing_alg_values_supported\":[\ \"RS256\",\ \"RS384\",\ \"RS512\",\ \"ES256\",\ \"ES384\",\ \"ES512\",\ \"HS256\",\ \"HS384\",\ \"HS512\",\ \"PS256\",\ \"PS384\",\ \"PS512\",\ \"none\"\ ],\ \"id_token_encryption_alg_values_supported\":[\ \"RSA1_5\",\ \"RSA-OAEP\",\ \"RSA-OAEP-256\",\ \"A128KW\",\ \"A192KW\",\ \"A256KW\",\ \"ECDH-ES\",\ \"ECDH-ES+A128KW\",\ \"ECDH-ES+A192KW\",\ \"ECDH-ES+A256KW\"\ ],\ \"id_token_encryption_enc_values_supported\":[\ \"A128CBC-HS256\",\ \"A192CBC-HS384\",\ \"A256CBC-HS512\",\ \"A128GCM\",\ \"A192GCM\",\ \"A256GCM\"\ ],\ \"userinfo_signing_alg_values_supported\":[\ \"RS256\",\ \"RS384\",\ \"RS512\",\ \"ES256\",\ \"ES384\",\ \"ES512\",\ \"HS256\",\ \"HS384\",\ \"HS512\",\ \"PS256\",\ \"PS384\",\ \"PS512\",\ \"none\"\ ],\ \"userinfo_encryption_alg_values_supported\":[\ \"RSA1_5\",\ \"RSA-OAEP\",\ \"RSA-OAEP-256\",\ \"A128KW\",\ \"A192KW\",\ \"A256KW\",\ \"ECDH-ES\",\ \"ECDH-ES+A128KW\",\ \"ECDH-ES+A192KW\",\ \"ECDH-ES+A256KW\"\ ],\ \"userinfo_encryption_enc_values_supported\":[\ \"A128CBC-HS256\",\ \"A192CBC-HS384\",\ \"A256CBC-HS512\",\ \"A128GCM\",\ \"A192GCM\",\ \"A256GCM\"\ ],\ \"request_object_signing_alg_values_supported\":[\ \"RS256\",\ \"RS384\",\ \"RS512\",\ \"ES256\",\ \"ES384\",\ \"ES512\",\ \"HS256\",\ \"HS384\",\ \"HS512\",\ \"PS256\",\ \"PS384\",\ \"PS512\",\ \"none\"\ ],\ \"request_object_encryption_alg_values_supported\":[\ \"RSA1_5\",\ \"RSA-OAEP\",\ \"RSA-OAEP-256\",\ \"A128KW\",\ \"A192KW\",\ \"A256KW\",\ \"ECDH-ES\",\ \"ECDH-ES+A128KW\",\ \"ECDH-ES+A192KW\",\ \"ECDH-ES+A256KW\"\ ],\ \"request_object_encryption_enc_values_supported\":[\ \"A128CBC-HS256\",\ \"A192CBC-HS384\",\ \"A256CBC-HS512\",\ \"A128GCM\",\ \"A192GCM\",\ \"A256GCM\"\ ],\ \"token_endpoint_auth_methods_supported\":[\ \"client_secret_post\",\ \"client_secret_basic\",\ \"client_secret_jwt\",\ \"private_key_jwt\"\ ],\ \"token_endpoint_auth_signing_alg_values_supported\":[\ \"RS256\",\ \"RS384\",\ \"RS512\",\ \"ES256\",\ \"ES384\",\ \"ES512\",\ \"HS256\",\ \"HS384\",\ \"HS512\",\ \"PS256\",\ \"PS384\",\ \"PS512\"\ ],\ \"claim_types_supported\":[\ \"normal\",\ \"aggregated\",\ \"distributed\"\ ],\ \"claims_supported\":[\ \"name\",\ \"given_name\",\ \"middle_name\",\ \"picture\",\ \"email_verified\",\ \"birthdate\",\ \"sub\",\ \"address\",\ \"zoneinfo\",\ \"email\",\ \"gender\",\ \"preferred_username\",\ \"family_name\",\ \"website\",\ \"profile\",\ \"phone_number_verified\",\ \"nickname\",\ \"updated_at\",\ \"phone_number\",\ \"locale\"\ ],\ \"claims_parameter_supported\":true,\ \"request_parameter_supported\":true,\ \"request_uri_parameter_supported\":true,\ \"require_request_uri_registration\":true"; let json_response = format!( "{{{},{}}}", json_response_standard, "\"end_session_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session\",\ \"version\":\"3.0\"" ); let all_signing_algs = vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, CoreJwsSigningAlgorithm::EcdsaP256Sha256, CoreJwsSigningAlgorithm::EcdsaP384Sha384, CoreJwsSigningAlgorithm::EcdsaP521Sha512, CoreJwsSigningAlgorithm::HmacSha256, CoreJwsSigningAlgorithm::HmacSha384, CoreJwsSigningAlgorithm::HmacSha512, CoreJwsSigningAlgorithm::RsaSsaPssSha256, CoreJwsSigningAlgorithm::RsaSsaPssSha384, CoreJwsSigningAlgorithm::RsaSsaPssSha512, CoreJwsSigningAlgorithm::None, ]; let all_encryption_algs = vec![ CoreJweKeyManagementAlgorithm::RsaPkcs1V15, CoreJweKeyManagementAlgorithm::RsaOaep, CoreJweKeyManagementAlgorithm::RsaOaepSha256, CoreJweKeyManagementAlgorithm::AesKeyWrap128, CoreJweKeyManagementAlgorithm::AesKeyWrap192, CoreJweKeyManagementAlgorithm::AesKeyWrap256, CoreJweKeyManagementAlgorithm::EcdhEs, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, ]; let new_provider_metadata = CoreProviderMetadata::new( IssuerUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" .to_string(), ) .unwrap(), AuthUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/\ rp-response_type-code/authorization" .to_string(), ) .unwrap(), JsonWebKeySetUrl::new( "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" .to_string(), ) .unwrap(), vec![ResponseTypes::new(vec![CoreResponseType::Code])], vec![ CoreSubjectIdentifierType::Public, CoreSubjectIdentifierType::Pairwise, ], all_signing_algs.clone(), Default::default(), ) .set_request_object_signing_alg_values_supported(Some(all_signing_algs.clone())) .set_token_endpoint_auth_signing_alg_values_supported(Some(vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, CoreJwsSigningAlgorithm::EcdsaP256Sha256, CoreJwsSigningAlgorithm::EcdsaP384Sha384, CoreJwsSigningAlgorithm::EcdsaP521Sha512, CoreJwsSigningAlgorithm::HmacSha256, CoreJwsSigningAlgorithm::HmacSha384, CoreJwsSigningAlgorithm::HmacSha512, CoreJwsSigningAlgorithm::RsaSsaPssSha256, CoreJwsSigningAlgorithm::RsaSsaPssSha384, CoreJwsSigningAlgorithm::RsaSsaPssSha512, ])) .set_scopes_supported(Some(vec![ Scope::new("email".to_string()), Scope::new("phone".to_string()), Scope::new("profile".to_string()), Scope::new("openid".to_string()), Scope::new("address".to_string()), Scope::new("offline_access".to_string()), Scope::new("openid".to_string()), ])) .set_userinfo_signing_alg_values_supported(Some(all_signing_algs)) .set_id_token_encryption_enc_values_supported(Some(vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, CoreJweContentEncryptionAlgorithm::Aes128Gcm, CoreJweContentEncryptionAlgorithm::Aes192Gcm, CoreJweContentEncryptionAlgorithm::Aes256Gcm, ])) .set_grant_types_supported(Some(vec![ CoreGrantType::AuthorizationCode, CoreGrantType::Implicit, CoreGrantType::JwtBearer, CoreGrantType::RefreshToken, ])) .set_response_modes_supported(Some(vec![ CoreResponseMode::Query, CoreResponseMode::Fragment, CoreResponseMode::FormPost, ])) .set_require_request_uri_registration(Some(true)) .set_registration_endpoint(Some( RegistrationUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/\ rp-response_type-code/registration" .to_string(), ) .unwrap(), )) .set_claims_parameter_supported(Some(true)) .set_request_object_encryption_enc_values_supported(Some(vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, CoreJweContentEncryptionAlgorithm::Aes128Gcm, CoreJweContentEncryptionAlgorithm::Aes192Gcm, CoreJweContentEncryptionAlgorithm::Aes256Gcm, ])) .set_userinfo_endpoint(Some( UserInfoUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/\ rp-response_type-code/userinfo" .to_string(), ) .unwrap(), )) .set_token_endpoint_auth_methods_supported(Some(vec![ CoreClientAuthMethod::ClientSecretPost, CoreClientAuthMethod::ClientSecretBasic, CoreClientAuthMethod::ClientSecretJwt, CoreClientAuthMethod::PrivateKeyJwt, ])) .set_claims_supported(Some( vec![ "name", "given_name", "middle_name", "picture", "email_verified", "birthdate", "sub", "address", "zoneinfo", "email", "gender", "preferred_username", "family_name", "website", "profile", "phone_number_verified", "nickname", "updated_at", "phone_number", "locale", ] .iter() .map(|claim| CoreClaimName::new((*claim).to_string())) .collect(), )) .set_request_object_encryption_alg_values_supported(Some(all_encryption_algs.clone())) .set_claim_types_supported(Some(vec![ CoreClaimType::Normal, CoreClaimType::Aggregated, CoreClaimType::Distributed, ])) .set_request_uri_parameter_supported(Some(true)) .set_request_parameter_supported(Some(true)) .set_token_endpoint(Some( TokenUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/\ rp-response_type-code/token" .to_string(), ) .unwrap(), )) .set_id_token_encryption_alg_values_supported(Some(all_encryption_algs.clone())) .set_userinfo_encryption_alg_values_supported(Some(all_encryption_algs)) .set_userinfo_encryption_enc_values_supported(Some(vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, CoreJweContentEncryptionAlgorithm::Aes128Gcm, CoreJweContentEncryptionAlgorithm::Aes192Gcm, CoreJweContentEncryptionAlgorithm::Aes256Gcm, ])) .set_acr_values_supported(Some(vec![AuthenticationContextClass::new( "PASSWORD".to_string(), )])); let provider_metadata: CoreProviderMetadata = serde_json::from_str(&json_response).unwrap(); assert_eq!(provider_metadata, new_provider_metadata); let serialized = serde_json::to_string(&provider_metadata).unwrap(); assert_eq!(serialized, format!("{{{}}}", json_response_standard)); assert_eq!( IssuerUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" .to_string() ) .unwrap(), *provider_metadata.issuer() ); assert_eq!( AuthUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ /authorization" .to_string() ) .unwrap(), *provider_metadata.authorization_endpoint() ); assert_eq!( Some( &TokenUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs\ /rp-response_type-code/token" .to_string() ) .unwrap() ), provider_metadata.token_endpoint() ); assert_eq!( Some( &UserInfoUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs\ /rp-response_type-code/userinfo" .to_string() ) .unwrap() ), provider_metadata.userinfo_endpoint() ); assert_eq!( &JsonWebKeySetUrl::new( "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" .to_string() ) .unwrap(), provider_metadata.jwks_uri() ); assert_eq!( Some( &RegistrationUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs\ /rp-response_type-code/registration" .to_string() ) .unwrap() ), provider_metadata.registration_endpoint() ); assert_eq!( Some( &[ "email", "phone", "profile", "openid", "address", "offline_access", "openid", ] .iter() .map(|s| (*s).to_string()) .map(Scope::new) .collect::>() ), provider_metadata.scopes_supported() ); assert_eq!( vec![ResponseTypes::new(vec![CoreResponseType::Code])], *provider_metadata.response_types_supported() ); assert_eq!( Some(&vec![ CoreResponseMode::Query, CoreResponseMode::Fragment, CoreResponseMode::FormPost, ]), provider_metadata.response_modes_supported() ); assert_eq!( Some( &vec![ CoreGrantType::AuthorizationCode, CoreGrantType::Implicit, CoreGrantType::JwtBearer, CoreGrantType::RefreshToken, ] .into_iter() .collect::>() ), provider_metadata.grant_types_supported() ); assert_eq!( Some(&vec![AuthenticationContextClass::new( "PASSWORD".to_string(), )]), provider_metadata.acr_values_supported() ); assert_eq!( vec![ CoreSubjectIdentifierType::Public, CoreSubjectIdentifierType::Pairwise, ], *provider_metadata.subject_types_supported() ); assert_eq!( vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, CoreJwsSigningAlgorithm::EcdsaP256Sha256, CoreJwsSigningAlgorithm::EcdsaP384Sha384, CoreJwsSigningAlgorithm::EcdsaP521Sha512, CoreJwsSigningAlgorithm::HmacSha256, CoreJwsSigningAlgorithm::HmacSha384, CoreJwsSigningAlgorithm::HmacSha512, CoreJwsSigningAlgorithm::RsaSsaPssSha256, CoreJwsSigningAlgorithm::RsaSsaPssSha384, CoreJwsSigningAlgorithm::RsaSsaPssSha512, CoreJwsSigningAlgorithm::None, ], *provider_metadata.id_token_signing_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweKeyManagementAlgorithm::RsaPkcs1V15, CoreJweKeyManagementAlgorithm::RsaOaep, CoreJweKeyManagementAlgorithm::RsaOaepSha256, CoreJweKeyManagementAlgorithm::AesKeyWrap128, CoreJweKeyManagementAlgorithm::AesKeyWrap192, CoreJweKeyManagementAlgorithm::AesKeyWrap256, CoreJweKeyManagementAlgorithm::EcdhEs, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, ]), provider_metadata.id_token_encryption_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, CoreJweContentEncryptionAlgorithm::Aes128Gcm, CoreJweContentEncryptionAlgorithm::Aes192Gcm, CoreJweContentEncryptionAlgorithm::Aes256Gcm, ]), provider_metadata.id_token_encryption_enc_values_supported() ); assert_eq!( Some(&vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, CoreJwsSigningAlgorithm::EcdsaP256Sha256, CoreJwsSigningAlgorithm::EcdsaP384Sha384, CoreJwsSigningAlgorithm::EcdsaP521Sha512, CoreJwsSigningAlgorithm::HmacSha256, CoreJwsSigningAlgorithm::HmacSha384, CoreJwsSigningAlgorithm::HmacSha512, CoreJwsSigningAlgorithm::RsaSsaPssSha256, CoreJwsSigningAlgorithm::RsaSsaPssSha384, CoreJwsSigningAlgorithm::RsaSsaPssSha512, CoreJwsSigningAlgorithm::None, ]), provider_metadata.userinfo_signing_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweKeyManagementAlgorithm::RsaPkcs1V15, CoreJweKeyManagementAlgorithm::RsaOaep, CoreJweKeyManagementAlgorithm::RsaOaepSha256, CoreJweKeyManagementAlgorithm::AesKeyWrap128, CoreJweKeyManagementAlgorithm::AesKeyWrap192, CoreJweKeyManagementAlgorithm::AesKeyWrap256, CoreJweKeyManagementAlgorithm::EcdhEs, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, ]), provider_metadata.userinfo_encryption_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, CoreJweContentEncryptionAlgorithm::Aes128Gcm, CoreJweContentEncryptionAlgorithm::Aes192Gcm, CoreJweContentEncryptionAlgorithm::Aes256Gcm, ]), provider_metadata.userinfo_encryption_enc_values_supported() ); assert_eq!( Some(&vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, CoreJwsSigningAlgorithm::EcdsaP256Sha256, CoreJwsSigningAlgorithm::EcdsaP384Sha384, CoreJwsSigningAlgorithm::EcdsaP521Sha512, CoreJwsSigningAlgorithm::HmacSha256, CoreJwsSigningAlgorithm::HmacSha384, CoreJwsSigningAlgorithm::HmacSha512, CoreJwsSigningAlgorithm::RsaSsaPssSha256, CoreJwsSigningAlgorithm::RsaSsaPssSha384, CoreJwsSigningAlgorithm::RsaSsaPssSha512, CoreJwsSigningAlgorithm::None, ]), provider_metadata.request_object_signing_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweKeyManagementAlgorithm::RsaPkcs1V15, CoreJweKeyManagementAlgorithm::RsaOaep, CoreJweKeyManagementAlgorithm::RsaOaepSha256, CoreJweKeyManagementAlgorithm::AesKeyWrap128, CoreJweKeyManagementAlgorithm::AesKeyWrap192, CoreJweKeyManagementAlgorithm::AesKeyWrap256, CoreJweKeyManagementAlgorithm::EcdhEs, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap192, CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap256, ]), provider_metadata.request_object_encryption_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256, CoreJweContentEncryptionAlgorithm::Aes192CbcHmacSha384, CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512, CoreJweContentEncryptionAlgorithm::Aes128Gcm, CoreJweContentEncryptionAlgorithm::Aes192Gcm, CoreJweContentEncryptionAlgorithm::Aes256Gcm, ]), provider_metadata.request_object_encryption_enc_values_supported() ); assert_eq!( Some(&vec![ CoreClientAuthMethod::ClientSecretPost, CoreClientAuthMethod::ClientSecretBasic, CoreClientAuthMethod::ClientSecretJwt, CoreClientAuthMethod::PrivateKeyJwt, ]), provider_metadata.token_endpoint_auth_methods_supported() ); assert_eq!( Some(&vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha512, CoreJwsSigningAlgorithm::EcdsaP256Sha256, CoreJwsSigningAlgorithm::EcdsaP384Sha384, CoreJwsSigningAlgorithm::EcdsaP521Sha512, CoreJwsSigningAlgorithm::HmacSha256, CoreJwsSigningAlgorithm::HmacSha384, CoreJwsSigningAlgorithm::HmacSha512, CoreJwsSigningAlgorithm::RsaSsaPssSha256, CoreJwsSigningAlgorithm::RsaSsaPssSha384, CoreJwsSigningAlgorithm::RsaSsaPssSha512, ]), provider_metadata.token_endpoint_auth_signing_alg_values_supported() ); assert_eq!(None, provider_metadata.display_values_supported()); assert_eq!( Some(&vec![ CoreClaimType::Normal, CoreClaimType::Aggregated, CoreClaimType::Distributed, ]), provider_metadata.claim_types_supported() ); assert_eq!( Some(&vec![ CoreClaimName::new("name".to_string()), CoreClaimName::new("given_name".to_string()), CoreClaimName::new("middle_name".to_string()), CoreClaimName::new("picture".to_string()), CoreClaimName::new("email_verified".to_string()), CoreClaimName::new("birthdate".to_string()), CoreClaimName::new("sub".to_string()), CoreClaimName::new("address".to_string()), CoreClaimName::new("zoneinfo".to_string()), CoreClaimName::new("email".to_string()), CoreClaimName::new("gender".to_string()), CoreClaimName::new("preferred_username".to_string()), CoreClaimName::new("family_name".to_string()), CoreClaimName::new("website".to_string()), CoreClaimName::new("profile".to_string()), CoreClaimName::new("phone_number_verified".to_string()), CoreClaimName::new("nickname".to_string()), CoreClaimName::new("updated_at".to_string()), CoreClaimName::new("phone_number".to_string()), CoreClaimName::new("locale".to_string()), ]), provider_metadata.claims_supported() ); assert_eq!(None, provider_metadata.service_documentation()); assert_eq!(None, provider_metadata.claims_locales_supported()); assert_eq!(None, provider_metadata.ui_locales_supported()); assert_eq!(Some(true), provider_metadata.claims_parameter_supported()); assert_eq!(Some(true), provider_metadata.request_parameter_supported()); assert_eq!( Some(true), provider_metadata.request_uri_parameter_supported() ); assert_eq!( Some(true), provider_metadata.require_request_uri_registration() ); assert_eq!(None, provider_metadata.op_policy_uri()); assert_eq!(None, provider_metadata.op_tos_uri()); // Note: the following fields provided by the response above are not part of the OpenID // Connect Discovery 1.0 spec: // - end_session_endpoint // - version let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); let redeserialized_metadata: CoreProviderMetadata = serde_json::from_str(&serialized_json).unwrap(); assert_eq!(provider_metadata, redeserialized_metadata); } // Tests the fields missing from the example response in test_discovery_deserialization(). #[test] fn test_discovery_deserialization_other_fields() { let json_response = "{ \"issuer\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\", \"authorization_endpoint\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\", \"jwks_uri\" : \"https://rp.certification.openid.net:8080/static/jwks_oMXD5waO08Q1GEnv.json\", \"response_types_supported\" : [ \"code\", \"code token\", \"code id_token\", \"id_token token\", \"code id_token token\", \"token id_token\", \"token id_token code\", \"id_token\", \"token\" ], \"subject_types_supported\" : [ \"public\", \"pairwise\" ], \"id_token_signing_alg_values_supported\" : [ \"HS256\", \"HS384\", \"HS512\" ], \"display_values_supported\" : [ \"page\", \"popup\", \"touch\", \"wap\" ], \"service_documentation\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/documentation\", \"claims_locales_supported\" : [ \"de\", \"fr\", \"de-CH-1901\" ], \"ui_locales_supported\" : [ \"ja\", \"sr-Latn\", \"yue-HK\" ], \"op_policy_uri\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/op_policy\", \"op_tos_uri\" : \"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/op_tos\" }"; let provider_metadata: CoreProviderMetadata = serde_json::from_str(json_response).unwrap(); assert_eq!( IssuerUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" .to_string() ) .unwrap(), *provider_metadata.issuer() ); assert_eq!( AuthUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ /authorization" .to_string() ) .unwrap(), *provider_metadata.authorization_endpoint() ); assert_eq!(None, provider_metadata.token_endpoint()); assert_eq!(None, provider_metadata.userinfo_endpoint()); assert_eq!( JsonWebKeySetUrl::new( "https://rp.certification.openid.net:8080/static/jwks_oMXD5waO08Q1GEnv.json" .to_string() ) .unwrap(), *provider_metadata.jwks_uri() ); assert_eq!(None, provider_metadata.registration_endpoint()); assert_eq!(None, provider_metadata.scopes_supported()); assert_eq!( vec![ ResponseTypes::new(vec![CoreResponseType::Code]), ResponseTypes::new(vec![CoreResponseType::Code, CoreResponseType::Token]), ResponseTypes::new(vec![CoreResponseType::Code, CoreResponseType::IdToken]), ResponseTypes::new(vec![CoreResponseType::IdToken, CoreResponseType::Token]), ResponseTypes::new(vec![ CoreResponseType::Code, CoreResponseType::IdToken, CoreResponseType::Token, ]), ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]), ResponseTypes::new(vec![ CoreResponseType::Token, CoreResponseType::IdToken, CoreResponseType::Code, ]), ResponseTypes::new(vec![CoreResponseType::IdToken]), ResponseTypes::new(vec![CoreResponseType::Token]), ], *provider_metadata.response_types_supported() ); assert_eq!(None, provider_metadata.response_modes_supported()); assert_eq!(None, provider_metadata.grant_types_supported()); assert_eq!(None, provider_metadata.acr_values_supported()); assert_eq!( vec![ CoreSubjectIdentifierType::Public, CoreSubjectIdentifierType::Pairwise, ], *provider_metadata.subject_types_supported() ); assert_eq!( vec![ CoreJwsSigningAlgorithm::HmacSha256, CoreJwsSigningAlgorithm::HmacSha384, CoreJwsSigningAlgorithm::HmacSha512, ], *provider_metadata.id_token_signing_alg_values_supported() ); assert_eq!( None, provider_metadata.id_token_encryption_alg_values_supported() ); assert_eq!( None, provider_metadata.id_token_encryption_enc_values_supported() ); assert_eq!( None, provider_metadata.userinfo_signing_alg_values_supported() ); assert_eq!( None, provider_metadata.userinfo_encryption_alg_values_supported() ); assert_eq!( None, provider_metadata.userinfo_encryption_enc_values_supported() ); assert_eq!( None, provider_metadata.request_object_signing_alg_values_supported() ); assert_eq!( None, provider_metadata.request_object_encryption_alg_values_supported() ); assert_eq!( None, provider_metadata.request_object_encryption_enc_values_supported() ); assert_eq!( None, provider_metadata.token_endpoint_auth_methods_supported() ); assert_eq!( None, provider_metadata.token_endpoint_auth_signing_alg_values_supported() ); assert_eq!( Some(&vec![ CoreAuthDisplay::Page, CoreAuthDisplay::Popup, CoreAuthDisplay::Touch, CoreAuthDisplay::Wap, ]), provider_metadata.display_values_supported() ); assert_eq!(None, provider_metadata.claim_types_supported()); assert_eq!(None, provider_metadata.claims_supported()); assert_eq!( Some( &ServiceDocUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ /documentation" .to_string() ) .unwrap() ), provider_metadata.service_documentation() ); assert_eq!( Some(&vec![ LanguageTag::new("de".to_string()), LanguageTag::new("fr".to_string()), LanguageTag::new("de-CH-1901".to_string()), ]), provider_metadata.claims_locales_supported() ); assert_eq!( Some(&vec![ LanguageTag::new("ja".to_string()), LanguageTag::new("sr-Latn".to_string()), LanguageTag::new("yue-HK".to_string()), ]), provider_metadata.ui_locales_supported() ); assert_eq!(None, provider_metadata.claims_parameter_supported()); assert_eq!(None, provider_metadata.request_parameter_supported()); assert_eq!(None, provider_metadata.request_uri_parameter_supported()); assert_eq!(None, provider_metadata.require_request_uri_registration()); assert_eq!( Some( &OpPolicyUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ /op_policy" .to_string() ) .unwrap() ), provider_metadata.op_policy_uri() ); assert_eq!( Some( &OpTosUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ /op_tos" .to_string() ) .unwrap() ), provider_metadata.op_tos_uri() ); let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); let redeserialized_metadata: CoreProviderMetadata = serde_json::from_str(&serialized_json).unwrap(); assert_eq!(provider_metadata, redeserialized_metadata); } // Tests that we ignore enum values that the OIDC provider supports but that the client does // not (which trigger serde deserialization errors while parsing the provider metadata). #[test] fn test_unsupported_enum_values() { let json_response = "{\ \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ \"response_types_supported\":[\ \"code\"\ ],\ \"subject_types_supported\":[\ \"public\",\ \"pairwise\"\ ],\ \"id_token_signing_alg_values_supported\":[\ \"RS256\",\ \"MAGIC\",\ \"none\"\ ],\ \"id_token_encryption_alg_values_supported\":[\ \"RSA1_5\",\ \"MAGIC\"\ ],\ \"id_token_encryption_enc_values_supported\":[\ \"A128CBC-HS256\",\ \"MAGIC\"\ ],\ \"userinfo_signing_alg_values_supported\":[\ \"RS256\",\ \"MAGIC\",\ \"none\"\ ],\ \"userinfo_encryption_alg_values_supported\":[\ \"RSA1_5\",\ \"MAGIC\"\ ],\ \"userinfo_encryption_enc_values_supported\":[\ \"A128CBC-HS256\",\ \"MAGIC\"\ ],\ \"request_object_signing_alg_values_supported\":[\ \"RS256\",\ \"MAGIC\",\ \"none\"\ ],\ \"request_object_encryption_alg_values_supported\":[\ \"RSA1_5\",\ \"MAGIC\"\ ],\ \"request_object_encryption_enc_values_supported\":[\ \"A128CBC-HS256\",\ \"MAGIC\"\ ],\ \"token_endpoint_auth_signing_alg_values_supported\":[\ \"RS256\",\ \"MAGIC\",\ \"none\"\ ]\ }"; let provider_metadata: CoreProviderMetadata = serde_json::from_str(json_response).unwrap(); assert_eq!( IssuerUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" .to_string() ) .unwrap(), *provider_metadata.issuer() ); assert_eq!( AuthUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\ /authorization" .to_string() ) .unwrap(), *provider_metadata.authorization_endpoint() ); assert_eq!(None, provider_metadata.token_endpoint()); assert_eq!(None, provider_metadata.userinfo_endpoint()); assert_eq!( JsonWebKeySetUrl::new( "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" .to_string() ) .unwrap(), *provider_metadata.jwks_uri() ); assert_eq!(None, provider_metadata.registration_endpoint()); assert_eq!(None, provider_metadata.scopes_supported()); assert_eq!( vec![ResponseTypes::new(vec![CoreResponseType::Code])], *provider_metadata.response_types_supported() ); assert_eq!(None, provider_metadata.response_modes_supported()); assert_eq!(None, provider_metadata.grant_types_supported()); assert_eq!(None, provider_metadata.acr_values_supported()); assert_eq!( vec![ CoreSubjectIdentifierType::Public, CoreSubjectIdentifierType::Pairwise, ], *provider_metadata.subject_types_supported() ); assert_eq!( vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::None, ], *provider_metadata.id_token_signing_alg_values_supported() ); assert_eq!( Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), provider_metadata.id_token_encryption_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 ]), provider_metadata.id_token_encryption_enc_values_supported() ); assert_eq!( Some(&vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::None, ]), provider_metadata.userinfo_signing_alg_values_supported() ); assert_eq!( Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), provider_metadata.userinfo_encryption_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 ]), provider_metadata.userinfo_encryption_enc_values_supported() ); assert_eq!( Some(&vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::None, ]), provider_metadata.request_object_signing_alg_values_supported() ); assert_eq!( Some(&vec![CoreJweKeyManagementAlgorithm::RsaPkcs1V15]), provider_metadata.request_object_encryption_alg_values_supported() ); assert_eq!( Some(&vec![ CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 ]), provider_metadata.request_object_encryption_enc_values_supported() ); assert_eq!( None, provider_metadata.token_endpoint_auth_methods_supported() ); assert_eq!( Some(&vec![ CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, CoreJwsSigningAlgorithm::None, ]), provider_metadata.token_endpoint_auth_signing_alg_values_supported() ); assert_eq!(None, provider_metadata.display_values_supported()); assert_eq!(None, provider_metadata.claim_types_supported()); assert_eq!(None, provider_metadata.claims_supported()); assert_eq!(None, provider_metadata.service_documentation()); assert_eq!(None, provider_metadata.claims_locales_supported()); assert_eq!(None, provider_metadata.ui_locales_supported()); assert_eq!(None, provider_metadata.claims_parameter_supported()); assert_eq!(None, provider_metadata.request_parameter_supported()); assert_eq!(None, provider_metadata.request_uri_parameter_supported()); assert_eq!(None, provider_metadata.require_request_uri_registration()); assert_eq!(None, provider_metadata.op_policy_uri()); assert_eq!(None, provider_metadata.op_tos_uri()); let serialized_json = serde_json::to_string(&provider_metadata).unwrap(); let redeserialized_metadata: CoreProviderMetadata = serde_json::from_str(&serialized_json).unwrap(); assert_eq!(provider_metadata, redeserialized_metadata); } ================================================ FILE: src/helpers.rs ================================================ use crate::types::localized::join_language_tag_key; use crate::{LanguageTag, LocalizedClaim}; use chrono::{DateTime, TimeZone, Utc}; use serde::de::value::MapDeserializer; use serde::de::{DeserializeOwned, Deserializer, Error, MapAccess, Visitor}; use serde::{Deserialize, Serialize, Serializer}; use serde_json::from_value; use serde_value::ValueDeserializer; use serde_with::{DeserializeAs, SerializeAs}; use std::cmp::PartialEq; use std::fmt::{Debug, Display, Formatter, Result as FormatterResult}; use std::marker::PhantomData; pub(crate) fn deserialize_string_or_vec<'de, T, D>(deserializer: D) -> Result, D::Error> where T: DeserializeOwned, D: Deserializer<'de>, { let value: serde_json::Value = Deserialize::deserialize(deserializer)?; match from_value::>(value.clone()) { Ok(val) => Ok(val), Err(_) => { let single_val: T = from_value(value).map_err(Error::custom)?; Ok(vec![single_val]) } } } pub(crate) fn deserialize_string_or_vec_opt<'de, T, D>( deserializer: D, ) -> Result>, D::Error> where T: DeserializeOwned, D: Deserializer<'de>, { let value: serde_json::Value = Deserialize::deserialize(deserializer)?; match from_value::>>(value.clone()) { Ok(val) => Ok(val), Err(_) => { let single_val: T = from_value(value).map_err(Error::custom)?; Ok(Some(vec![single_val])) } } } // Attempt to deserialize the value; if the value is null or an error occurs, return None. // This is useful when deserializing fields that may mean different things in different // contexts, and where we would rather ignore the result than fail to deserialize. For example, // the fields in JWKs are not well defined; extensions could theoretically define their own // field names that overload field names used by other JWK types. pub(crate) fn deserialize_option_or_none<'de, T, D>(deserializer: D) -> Result, D::Error> where T: DeserializeOwned, D: Deserializer<'de>, { let value: serde_json::Value = Deserialize::deserialize(deserializer)?; match from_value::>(value) { Ok(val) => Ok(val), Err(_) => Ok(None), } } pub trait DeserializeMapField: Sized { fn deserialize_map_field<'de, V>( map: &mut V, field_name: &'static str, language_tag: Option, field_value: Option, ) -> Result where V: MapAccess<'de>; } impl DeserializeMapField for T where T: DeserializeOwned, { fn deserialize_map_field<'de, V>( map: &mut V, field_name: &'static str, language_tag: Option, field_value: Option, ) -> Result where V: MapAccess<'de>, { if field_value.is_some() { return Err(serde::de::Error::duplicate_field(field_name)); } else if let Some(language_tag) = language_tag { return Err(serde::de::Error::custom(format!( "unexpected language tag `{language_tag}` for key `{field_name}`" ))); } map.next_value().map_err(|err| { V::Error::custom(format!( "{}: {err}", join_language_tag_key(field_name, language_tag.as_ref()) )) }) } } impl DeserializeMapField for LocalizedClaim where T: DeserializeOwned, { fn deserialize_map_field<'de, V>( map: &mut V, field_name: &'static str, language_tag: Option, field_value: Option, ) -> Result where V: MapAccess<'de>, { let mut localized_claim = field_value.unwrap_or_default(); if localized_claim.contains_key(language_tag.as_ref()) { return Err(serde::de::Error::custom(format!( "duplicate field `{}`", join_language_tag_key(field_name, language_tag.as_ref()) ))); } let localized_value = map.next_value().map_err(|err| { V::Error::custom(format!( "{}: {err}", join_language_tag_key(field_name, language_tag.as_ref()) )) })?; localized_claim.insert(language_tag, localized_value); Ok(localized_claim) } } // Some providers return boolean values as strings. Provide support for // parsing using stdlib. #[cfg(feature = "accept-string-booleans")] pub(crate) mod serde_string_bool { use serde::{de, Deserializer}; use std::fmt; pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { struct BooleanLikeVisitor; impl<'de> de::Visitor<'de> for BooleanLikeVisitor { type Value = bool; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("A boolean-like value") } fn visit_bool(self, v: bool) -> Result where E: de::Error, { Ok(v) } fn visit_str(self, v: &str) -> Result where E: de::Error, { v.parse().map_err(E::custom) } } deserializer.deserialize_any(BooleanLikeVisitor) } } /// Serde space-delimited string serializer for an `Option>`. /// /// This function serializes a string vector into a single space-delimited string. /// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` /// in the case of JSON serialization). pub(crate) fn serialize_space_delimited_vec( vec: &[T], serializer: S, ) -> Result where T: AsRef, S: Serializer, { let space_delimited = vec .iter() .map(AsRef::::as_ref) .collect::>() .join(" "); serializer.serialize_str(&space_delimited) } pub(crate) trait FlattenFilter { fn should_include(field_name: &str) -> bool; } /// Helper container for filtering map keys out of serde(flatten). This is needed because /// [`crate::StandardClaims`] doesn't have a fixed set of field names due to its support for /// localized claims. Consequently, serde by default passes all of the claims to the deserializer /// for `AC` (additional claims), leading to duplicate claims. [`FilteredFlatten`] is used for /// eliminating the duplicate claims. #[derive(Serialize)] pub(crate) struct FilteredFlatten where F: FlattenFilter, T: DeserializeOwned + Serialize, { // We include another level of flattening here because the derived flatten // ([`serde::private::de::FlatMapDeserializer`]) seems to support a wider set of types // (e.g., various forms of enum tagging) than [`serde_value::ValueDeserializer`]. #[serde(flatten)] inner: Flatten, #[serde(skip)] _phantom: PhantomData, } impl From for FilteredFlatten where F: FlattenFilter, T: DeserializeOwned + Serialize, { fn from(value: T) -> Self { Self { inner: Flatten { inner: value }, _phantom: PhantomData, } } } impl AsRef for FilteredFlatten where F: FlattenFilter, T: DeserializeOwned + Serialize, { fn as_ref(&self) -> &T { self.inner.as_ref() } } impl AsMut for FilteredFlatten where F: FlattenFilter, T: DeserializeOwned + Serialize, { fn as_mut(&mut self) -> &mut T { self.inner.as_mut() } } impl PartialEq for FilteredFlatten where F: FlattenFilter, T: DeserializeOwned + PartialEq + Serialize, { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl Clone for FilteredFlatten where F: FlattenFilter, T: Clone + DeserializeOwned + Serialize, { fn clone(&self) -> Self { Self { inner: Flatten { inner: self.inner.inner.clone(), }, _phantom: PhantomData, } } } impl Debug for FilteredFlatten where F: FlattenFilter, T: Debug + DeserializeOwned + Serialize, { // Transparent Debug since we don't care about this struct. fn fmt(&self, f: &mut Formatter) -> FormatterResult { Debug::fmt(&self.inner, f) } } impl<'de, F, T> Deserialize<'de> for FilteredFlatten where F: FlattenFilter, T: DeserializeOwned + Serialize, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct MapVisitor(PhantomData<(F, T)>); impl<'de, F, T> Visitor<'de> for MapVisitor where F: FlattenFilter, T: DeserializeOwned + Serialize, { type Value = Flatten; fn expecting(&self, formatter: &mut Formatter) -> FormatterResult { formatter.write_str("map type T") } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'de>, { let mut entries = Vec::<(serde_value::Value, serde_value::Value)>::new(); // JSON only supports String keys, and we really only need to support JSON input. while let Some(key) = map.next_key::()? { let key_str = String::deserialize(ValueDeserializer::new(key.clone()))?; if F::should_include(&key_str) { entries.push((key, map.next_value()?)); } } Deserialize::deserialize(MapDeserializer::new(entries.into_iter())) .map_err(serde_value::DeserializerError::into_error) } } Ok(FilteredFlatten { inner: deserializer.deserialize_map(MapVisitor(PhantomData::<(F, T)>))?, _phantom: PhantomData, }) } } #[derive(Deserialize, Serialize)] struct Flatten where T: DeserializeOwned + Serialize, { #[serde(flatten, bound = "T: DeserializeOwned + Serialize")] inner: T, } impl AsRef for Flatten where T: DeserializeOwned + Serialize, { fn as_ref(&self) -> &T { &self.inner } } impl AsMut for Flatten where T: DeserializeOwned + Serialize, { fn as_mut(&mut self) -> &mut T { &mut self.inner } } impl PartialEq for Flatten where T: DeserializeOwned + PartialEq + Serialize, { fn eq(&self, other: &Self) -> bool { self.inner == other.inner } } impl Debug for Flatten where T: Debug + DeserializeOwned + Serialize, { // Transparent Debug since we don't care about this struct. fn fmt(&self, f: &mut Formatter) -> FormatterResult { Debug::fmt(&self.inner, f) } } pub(crate) fn join_vec(entries: &[T]) -> String where T: AsRef, { entries .iter() .map(AsRef::as_ref) .collect::>() .join(" ") } /// Newtype around a bool, optionally supporting string values. #[derive(Debug, Deserialize, Serialize)] #[serde(transparent)] pub(crate) struct Boolean( #[cfg_attr( feature = "accept-string-booleans", serde(deserialize_with = "crate::helpers::serde_string_bool::deserialize") )] pub bool, ); impl Boolean { pub(crate) fn into_inner(self) -> bool { self.0 } } impl Display for Boolean { fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { Display::fmt(&self.0, f) } } /// Timestamp as seconds since the unix epoch, or optionally an ISO 8601 string. #[derive(Debug, Deserialize, Serialize)] #[serde(untagged)] pub(crate) enum Timestamp { Seconds(serde_json::Number), #[cfg(feature = "accept-rfc3339-timestamps")] Rfc3339(String), } impl Timestamp { // The spec is ambiguous about whether seconds should be expressed as integers, or // whether floating-point values are allowed. For compatibility with a wide range of // clients, we round down to the nearest second. pub(crate) fn from_utc(utc: &DateTime) -> Self { Timestamp::Seconds(utc.timestamp().into()) } pub(crate) fn to_utc(&self) -> Result, ()> { match self { Timestamp::Seconds(seconds) => { let (secs, nsecs) = if seconds.is_i64() { (seconds.as_i64().ok_or(())?, 0u32) } else { let secs_f64 = seconds.as_f64().ok_or(())?; let secs = secs_f64.floor(); ( secs as i64, ((secs_f64 - secs) * 1_000_000_000.).floor() as u32, ) }; Utc.timestamp_opt(secs, nsecs).single().ok_or(()) } #[cfg(feature = "accept-rfc3339-timestamps")] Timestamp::Rfc3339(iso) => { let datetime = DateTime::parse_from_rfc3339(iso).map_err(|_| ())?; Ok(datetime.into()) } } } } impl Display for Timestamp { fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { match self { Timestamp::Seconds(seconds) => Display::fmt(seconds, f), #[cfg(feature = "accept-rfc3339-timestamps")] Timestamp::Rfc3339(iso) => Display::fmt(iso, f), } } } impl<'de> DeserializeAs<'de, DateTime> for Timestamp { fn deserialize_as(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let seconds: Timestamp = Deserialize::deserialize(deserializer)?; seconds.to_utc().map_err(|_| { serde::de::Error::custom(format!( "failed to parse `{}` as UTC datetime (in seconds)", seconds )) }) } } impl SerializeAs> for Timestamp { fn serialize_as(source: &DateTime, serializer: S) -> Result where S: Serializer, { Timestamp::from_utc(source).serialize(serializer) } } new_type![ #[derive(Deserialize, Hash, Serialize)] pub(crate) Base64UrlEncodedBytes( #[serde(with = "serde_base64url_byte_array")] Vec ) ]; mod serde_base64url_byte_array { use crate::core::base64_url_safe_no_pad; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use serde::de::Error; use serde::{Deserialize, Deserializer, Serializer}; use serde_json::{from_value, Value}; pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let value: Value = Deserialize::deserialize(deserializer)?; let base64_encoded: String = from_value(value).map_err(D::Error::custom)?; base64_url_safe_no_pad() .decode(&base64_encoded) .map_err(|err| { D::Error::custom(format!( "invalid base64url encoding `{}`: {:?}", base64_encoded, err )) }) } pub fn serialize(v: &[u8], serializer: S) -> Result where S: Serializer, { let base64_encoded = BASE64_URL_SAFE_NO_PAD.encode(v); serializer.serialize_str(&base64_encoded) } } ================================================ FILE: src/http_utils.rs ================================================ use crate::AccessToken; use http::header::{HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; pub const MIME_TYPE_JSON: &str = "application/json"; pub const MIME_TYPE_JWKS: &str = "application/jwk-set+json"; pub const MIME_TYPE_JWT: &str = "application/jwt"; pub const BEARER: &str = "Bearer"; // The [essence](https://mimesniff.spec.whatwg.org/#mime-type-essence) is the / // representation. pub fn content_type_has_essence(content_type: &HeaderValue, expected_essence: &str) -> bool { #[allow(clippy::or_fun_call)] content_type .to_str() .ok() .filter(|ct| { ct[..ct.find(';').unwrap_or(ct.len())].to_lowercase() == expected_essence.to_lowercase() }) .is_some() } pub fn check_content_type(headers: &HeaderMap, expected_content_type: &str) -> Result<(), String> { headers .get(CONTENT_TYPE) .map_or(Ok(()), |content_type| // Section 3.1.1.1 of RFC 7231 indicates that media types are case insensitive and // may be followed by optional whitespace and/or a parameter (e.g., charset). // See https://tools.ietf.org/html/rfc7231#section-3.1.1.1. if !content_type_has_essence(content_type, expected_content_type) { Err( format!( "Unexpected response Content-Type: {:?}, should be `{}`", content_type, expected_content_type ) ) } else { Ok(()) } ) } pub fn auth_bearer(access_token: &AccessToken) -> (HeaderName, HeaderValue) { ( AUTHORIZATION, HeaderValue::from_str(&format!("{} {}", BEARER, access_token.secret())) .expect("invalid access token"), ) } ================================================ FILE: src/id_token/mod.rs ================================================ use crate::helpers::{deserialize_string_or_vec, FilteredFlatten, Timestamp}; use crate::jwt::JsonWebTokenAccess; use crate::jwt::{JsonWebTokenError, JsonWebTokenJsonPayloadSerde}; use crate::types::jwk::JwsSigningAlgorithm; use crate::{ AccessToken, AccessTokenHash, AdditionalClaims, AddressClaim, Audience, AudiencesClaim, AuthenticationContextClass, AuthenticationMethodReference, AuthorizationCode, AuthorizationCodeHash, ClaimsVerificationError, ClientId, EndUserBirthday, EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, EndUserWebsiteUrl, ExtraTokenFields, GenderClaim, IdTokenVerifier, IssuerClaim, IssuerUrl, JsonWebKey, JsonWebToken, JsonWebTokenAlgorithm, JweContentEncryptionAlgorithm, LanguageTag, LocalizedClaim, Nonce, NonceVerifier, PrivateSigningKey, SignatureVerificationError, StandardClaims, SubjectIdentifier, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_with::{serde_as, skip_serializing_none}; use std::fmt::Debug; use std::str::FromStr; #[cfg(test)] mod tests; // This wrapper layer exists instead of directly verifying the JWT and returning the claims so that // we can pass it around and easily access a serialized JWT representation of it (e.g., for passing // to the authorization endpoint as an id_token_hint). /// OpenID Connect ID token. #[cfg_attr( any(test, feature = "timing-resistant-secret-traits"), derive(PartialEq) )] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct IdToken< AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, >( #[serde(bound = "AC: AdditionalClaims")] JsonWebToken, JsonWebTokenJsonPayloadSerde>, ); impl FromStr for IdToken where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { type Err = serde_json::Error; fn from_str(s: &str) -> Result { serde_json::from_value(Value::String(s.to_string())) } } impl IdToken where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { /// Initializes an ID token with the specified claims, signed using the given signing key and /// algorithm. /// /// If an `access_token` and/or `code` are provided, this method sets the `at_hash` and/or /// `c_hash` claims using the given signing algorithm, respectively. Otherwise, those claims are /// unchanged from the values specified in `claims`. pub fn new( claims: IdTokenClaims, signing_key: &S, alg: JS, access_token: Option<&AccessToken>, code: Option<&AuthorizationCode>, ) -> Result where S: PrivateSigningKey, ::VerificationKey: JsonWebKey, { let verification_key = signing_key.as_verification_key(); let at_hash = access_token .map(|at| { AccessTokenHash::from_token(at, &alg, &verification_key) .map_err(JsonWebTokenError::SigningError) }) .transpose()? .or_else(|| claims.access_token_hash.clone()); let c_hash = code .map(|c| { AuthorizationCodeHash::from_code(c, &alg, &verification_key) .map_err(JsonWebTokenError::SigningError) }) .transpose()? .or_else(|| claims.code_hash.clone()); JsonWebToken::new( IdTokenClaims { access_token_hash: at_hash, code_hash: c_hash, ..claims }, signing_key, &alg, ) .map(Self) } /// Verifies and returns a reference to the ID token claims. pub fn claims<'a, K, N>( &'a self, verifier: &IdTokenVerifier, nonce_verifier: N, ) -> Result<&'a IdTokenClaims, ClaimsVerificationError> where K: JsonWebKey, N: NonceVerifier, { verifier.verified_claims(&self.0, nonce_verifier) } /// Verifies and returns the ID token claims. pub fn into_claims( self, verifier: &IdTokenVerifier, nonce_verifier: N, ) -> Result, ClaimsVerificationError> where K: JsonWebKey, N: NonceVerifier, { verifier.verified_claims_owned(self.0, nonce_verifier) } /// Returns the [`JwsSigningAlgorithm`] used to sign this ID token. /// /// This function returns an error if the token is unsigned or utilizes JSON Web Encryption /// (JWE). pub fn signing_alg(&self) -> Result<&JS, SignatureVerificationError> { match self.0.unverified_header().alg { JsonWebTokenAlgorithm::Signature(ref signing_alg) => Ok(signing_alg), JsonWebTokenAlgorithm::Encryption(ref other) => { Err(SignatureVerificationError::UnsupportedAlg( serde_plain::to_string(other).unwrap_or_else(|err| { panic!( "encryption alg {:?} failed to serialize to a string: {}", other, err ) }), )) } JsonWebTokenAlgorithm::None => Err(SignatureVerificationError::NoSignature), } } /// Returns the [`JsonWebKey`] usable for verifying this ID token's JSON Web Signature. /// /// This function returns an error if the token has no signature or a corresponding key cannot /// be found. pub fn signing_key<'s, K>( &self, verifier: &'s IdTokenVerifier<'s, K>, ) -> Result<&'s K, SignatureVerificationError> where K: JsonWebKey, { verifier .jwt_verifier .signing_key(self.0.unverified_header().kid.as_ref(), self.signing_alg()?) } } impl ToString for IdToken where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { fn to_string(&self) -> String { serde_json::to_value(self) // This should never arise, since we're just asking serde_json to serialize the // signing input concatenated with the signature, both of which are precomputed. .expect("ID token serialization failed") .as_str() // This should also never arise, since our IdToken serializer always calls serialize_str .expect("ID token serializer did not produce a str") .to_owned() } } /// OpenID Connect ID token claims. #[cfg_attr( any(test, feature = "timing-resistant-secret-traits"), derive(PartialEq) )] #[serde_as] #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct IdTokenClaims where AC: AdditionalClaims, GC: GenderClaim, { #[serde(rename = "iss")] issuer: IssuerUrl, // We always serialize as an array, which is valid according to the spec. This sets the // 'default' attribute to be compatible with non-spec compliant OIDC providers that omit this // field. #[serde( default, rename = "aud", deserialize_with = "deserialize_string_or_vec" )] audiences: Vec, #[serde_as(as = "Timestamp")] #[serde(rename = "exp")] expiration: DateTime, #[serde_as(as = "Timestamp")] #[serde(rename = "iat")] issue_time: DateTime, #[serde_as(as = "Option")] auth_time: Option>, nonce: Option, #[serde(rename = "acr")] auth_context_ref: Option, #[serde(rename = "amr")] auth_method_refs: Option>, #[serde(rename = "azp")] authorized_party: Option, #[serde(rename = "at_hash")] access_token_hash: Option, #[serde(rename = "c_hash")] code_hash: Option, #[serde(bound = "GC: GenderClaim")] #[serde(flatten)] standard_claims: StandardClaims, #[serde(bound = "AC: AdditionalClaims")] #[serde(flatten)] additional_claims: FilteredFlatten, AC>, } impl IdTokenClaims where AC: AdditionalClaims, GC: GenderClaim, { /// Initializes new ID token claims. pub fn new( issuer: IssuerUrl, audiences: Vec, expiration: DateTime, issue_time: DateTime, standard_claims: StandardClaims, additional_claims: AC, ) -> Self { Self { issuer, audiences, expiration, issue_time, auth_time: None, nonce: None, auth_context_ref: None, auth_method_refs: None, authorized_party: None, access_token_hash: None, code_hash: None, standard_claims, additional_claims: additional_claims.into(), } } field_getters_setters![ pub self [self] ["claim"] { set_issuer -> issuer[IssuerUrl] ["iss"], set_audiences -> audiences[Vec] ["aud"], set_expiration -> expiration[DateTime] ["exp"], set_issue_time -> issue_time[DateTime] ["iat"], set_auth_time -> auth_time[Option>], set_nonce -> nonce[Option], set_auth_context_ref -> auth_context_ref[Option] ["acr"], set_auth_method_refs -> auth_method_refs[Option>] ["amr"], set_authorized_party -> authorized_party[Option] ["azp"], set_access_token_hash -> access_token_hash[Option] ["at_hash"], set_code_hash -> code_hash[Option] ["c_hash"], } ]; /// Returns the `sub` claim. pub fn subject(&self) -> &SubjectIdentifier { &self.standard_claims.sub } /// Sets the `sub` claim. pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self { self.standard_claims.sub = subject; self } field_getters_setters![ pub self [self.standard_claims] ["claim"] { set_name -> name[Option>], set_given_name -> given_name[Option>], set_family_name -> family_name[Option>], set_middle_name -> middle_name[Option>], set_nickname -> nickname[Option>], set_preferred_username -> preferred_username[Option], set_profile -> profile[Option>], set_picture -> picture[Option>], set_website -> website[Option>], set_email -> email[Option], set_email_verified -> email_verified[Option], set_gender -> gender[Option], set_birthday -> birthday[Option], set_birthdate -> birthdate[Option], set_zoneinfo -> zoneinfo[Option], set_locale -> locale[Option], set_phone_number -> phone_number[Option], set_phone_number_verified -> phone_number_verified[Option], set_address -> address[Option], set_updated_at -> updated_at[Option>], } ]; /// Returns additional ID token claims. pub fn additional_claims(&self) -> &AC { self.additional_claims.as_ref() } /// Returns mutable additional ID token claims. pub fn additional_claims_mut(&mut self) -> &mut AC { self.additional_claims.as_mut() } } impl AudiencesClaim for IdTokenClaims where AC: AdditionalClaims, GC: GenderClaim, { fn audiences(&self) -> Option<&Vec> { Some(IdTokenClaims::audiences(self)) } } impl<'a, AC, GC> AudiencesClaim for &'a IdTokenClaims where AC: AdditionalClaims, GC: GenderClaim, { fn audiences(&self) -> Option<&Vec> { Some(IdTokenClaims::audiences(self)) } } impl IssuerClaim for IdTokenClaims where AC: AdditionalClaims, GC: GenderClaim, { fn issuer(&self) -> Option<&IssuerUrl> { Some(IdTokenClaims::issuer(self)) } } impl<'a, AC, GC> IssuerClaim for &'a IdTokenClaims where AC: AdditionalClaims, GC: GenderClaim, { fn issuer(&self) -> Option<&IssuerUrl> { Some(IdTokenClaims::issuer(self)) } } /// Extends the base OAuth2 token response with an ID token. #[cfg_attr( any(test, feature = "timing-resistant-secret-traits"), derive(PartialEq) )] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct IdTokenFields where AC: AdditionalClaims, EF: ExtraTokenFields, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { #[serde(bound = "AC: AdditionalClaims")] id_token: Option>, #[serde(bound = "EF: ExtraTokenFields", flatten)] extra_fields: EF, } impl IdTokenFields where AC: AdditionalClaims, EF: ExtraTokenFields, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { /// Initializes new ID token fields containing the specified [`IdToken`] and extra fields. pub fn new(id_token: Option>, extra_fields: EF) -> Self { Self { id_token, extra_fields, } } /// Returns the [`IdToken`] contained in the OAuth2 token response. pub fn id_token(&self) -> Option<&IdToken> { self.id_token.as_ref() } /// Returns the extra fields contained in the OAuth2 token response. pub fn extra_fields(&self) -> &EF { &self.extra_fields } } impl ExtraTokenFields for IdTokenFields where AC: AdditionalClaims, EF: ExtraTokenFields, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { } ================================================ FILE: src/id_token/tests.rs ================================================ use crate::claims::{AdditionalClaims, EmptyAdditionalClaims, StandardClaims}; use crate::core::{ CoreGenderClaim, CoreIdToken, CoreIdTokenClaims, CoreTokenResponse, CoreTokenType, }; use crate::jwt::JsonWebTokenAccess; use crate::{ AccessTokenHash, AddressClaim, AddressCountry, AddressLocality, AddressPostalCode, AddressRegion, Audience, AudiencesClaim, AuthenticationContextClass, AuthenticationMethodReference, AuthorizationCodeHash, ClientId, EndUserBirthday, EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, EndUserWebsiteUrl, FormattedAddress, IdTokenClaims, IssuerClaim, IssuerUrl, LanguageTag, Nonce, StreetAddress, SubjectIdentifier, }; use chrono::{TimeZone, Utc}; use oauth2::TokenResponse; use pretty_assertions::assert_eq; use serde::{Deserialize, Serialize}; use url::Url; use std::collections::HashMap; use std::str::FromStr; #[test] fn test_id_token() { static ID_TOKEN: &str = concat!( "eyJhbGciOiJSUzI1NiJ9.", "eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsImF1ZCI6WyJzNkJoZ", "FJrcXQzIl0sImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJzdWIiOi", "IyNDQwMDMyMCIsInRmYV9tZXRob2QiOiJ1MmYifQ.", "aW52YWxpZF9zaWduYXR1cmU" ); // `serde::Deserialize` implementation is tested within the `FromStr` implementation let id_token = CoreIdToken::from_str(ID_TOKEN).expect("failed to parse id_token"); let claims = id_token.0.unverified_payload_ref(); assert_eq!( *claims.issuer().url(), Url::parse("https://server.example.com").unwrap() ); assert_eq!( *claims.audiences(), vec![Audience::new("s6BhdRkqt3".to_string())] ); assert_eq!( claims.expiration(), Utc.timestamp_opt(1311281970, 0) .single() .expect("valid timestamp") ); assert_eq!( claims.issue_time(), Utc.timestamp_opt(1311280970, 0) .single() .expect("valid timestamp") ); assert_eq!( *claims.subject(), SubjectIdentifier::new("24400320".to_string()) ); // test `ToString` implementation assert_eq!(&id_token.to_string(), ID_TOKEN); // test `serde::Serialize` implementation too let de = serde_json::to_string(&id_token).expect("failed to deserializee id token"); assert_eq!(de, format!("\"{}\"", ID_TOKEN)); } #[test] fn test_oauth2_response() { let response_str = "{\ \"access_token\":\"foobar\",\ \"token_type\":\"bearer\",\ \"id_token\":\"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsImF\ 1ZCI6WyJzNkJoZFJrcXQzIl0sImV4cCI6MTMxMTI4MTk3MCwiaWF0IjoxMzExMjgwOTcwLCJzdWIiOiIyNDQwMD\ MyMCIsInRmYV9tZXRob2QiOiJ1MmYifQ.aW52YWxpZF9zaWduYXR1cmU\"\ }"; let response = serde_json::from_str::(response_str).expect("failed to deserialize"); assert_eq!(*response.access_token().secret(), "foobar"); assert_eq!(*response.token_type(), CoreTokenType::Bearer); let id_token = response.extra_fields().id_token(); let claims = id_token.unwrap().0.unverified_payload_ref(); assert_eq!( *claims.issuer().url(), Url::parse("https://server.example.com").unwrap() ); assert_eq!( *claims.audiences(), vec![Audience::new("s6BhdRkqt3".to_string())] ); assert_eq!( claims.expiration(), Utc.timestamp_opt(1311281970, 0) .single() .expect("valid timestamp") ); assert_eq!( claims.issue_time(), Utc.timestamp_opt(1311280970, 0) .single() .expect("valid timestamp") ); assert_eq!( *claims.subject(), SubjectIdentifier::new("24400320".to_string()) ); assert_eq!( serde_json::to_string(&response).expect("failed to serialize"), response_str ); } #[test] fn test_minimal_claims_serde() { let new_claims = CoreIdTokenClaims::new( IssuerUrl::new("https://server.example.com".to_string()).unwrap(), vec![Audience::new("s6BhdRkqt3".to_string())], Utc.timestamp_opt(1311281970, 0) .single() .expect("valid timestamp"), Utc.timestamp_opt(1311280970, 0) .single() .expect("valid timestamp"), StandardClaims::new(SubjectIdentifier::new("24400320".to_string())), EmptyAdditionalClaims {}, ); let expected_serialized_claims = "\ {\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\"],\ \"exp\":1311281970,\ \"iat\":1311280970,\ \"sub\":\"24400320\"\ }"; let new_serialized_claims = serde_json::to_string(&new_claims).expect("failed to serialize"); assert_eq!(new_serialized_claims, expected_serialized_claims); let claims: CoreIdTokenClaims = serde_json::from_str( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": \"s6BhdRkqt3\", \"exp\": 1311281970, \"iat\": 1311280970 }", ) .expect("failed to deserialize"); assert_eq!(claims, new_claims); assert_eq!(claims.issuer().url(), new_claims.issuer().url()); assert_eq!(claims.audiences(), new_claims.audiences()); assert_eq!(claims.expiration(), new_claims.expiration()); assert_eq!(claims.issue_time(), new_claims.issue_time()); assert_eq!(claims.auth_time(), None); assert!(claims.nonce().is_none()); assert_eq!(claims.auth_context_ref(), None); assert_eq!(claims.auth_method_refs(), None); assert_eq!(claims.authorized_party(), None); assert_eq!(claims.access_token_hash(), None); assert_eq!(claims.code_hash(), None); assert_eq!(*claims.additional_claims(), EmptyAdditionalClaims {}); assert_eq!(claims.subject(), new_claims.subject()); assert_eq!(claims.name(), None); assert_eq!(claims.given_name(), None); assert_eq!(claims.family_name(), None); assert_eq!(claims.middle_name(), None); assert_eq!(claims.nickname(), None); assert_eq!(claims.preferred_username(), None); assert_eq!(claims.profile(), None); assert_eq!(claims.picture(), None); assert_eq!(claims.website(), None); assert_eq!(claims.email(), None); assert_eq!(claims.email_verified(), None); assert_eq!(claims.gender(), None); assert_eq!(claims.birthday(), None); assert_eq!(claims.birthdate(), None); assert_eq!(claims.zoneinfo(), None); assert_eq!(claims.locale(), None); assert_eq!(claims.phone_number(), None); assert_eq!(claims.phone_number_verified(), None); assert_eq!(claims.address(), None); assert_eq!(claims.updated_at(), None); let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); assert_eq!(serialized_claims, expected_serialized_claims); let claims_round_trip: CoreIdTokenClaims = serde_json::from_str(&serialized_claims).expect("failed to deserialize"); assert_eq!(claims, claims_round_trip); } #[test] fn test_complete_claims_serde() { let claims_json = "{\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\"],\ \"exp\":1311281970,\ \"iat\":1311280970,\ \"auth_time\":1311282970,\ \"nonce\":\"Zm9vYmFy\",\ \"acr\":\"urn:mace:incommon:iap:silver\",\ \"amr\":[\"password\",\"totp\"],\ \"azp\":\"dGhpc19jbGllbnQ\",\ \"at_hash\":\"_JPLB-GtkomFJxAOWKHPHQ\",\ \"c_hash\":\"VpTQii5T_8rgwxA-Wtb2Bw\",\ \"sub\":\"24400320\",\ \"name\":\"Homer Simpson\",\ \"name#es\":\"Jomer Simpson\",\ \"given_name\":\"Homer\",\ \"given_name#es\":\"Jomer\",\ \"family_name\":\"Simpson\",\ \"family_name#es\":\"Simpson\",\ \"middle_name\":\"Jay\",\ \"middle_name#es\":\"Jay\",\ \"nickname\":\"Homer\",\ \"nickname#es\":\"Jomer\",\ \"preferred_username\":\"homersimpson\",\ \"profile\":\"https://example.com/profile?id=12345\",\ \"profile#es\":\"https://example.com/profile?id=12345&lang=es\",\ \"picture\":\"https://example.com/avatar?id=12345\",\ \"picture#es\":\"https://example.com/avatar?id=12345&lang=es\",\ \"website\":\"https://homersimpson.me\",\ \"website#es\":\"https://homersimpson.me/?lang=es\",\ \"email\":\"homer@homersimpson.me\",\ \"email_verified\":true,\ \"gender\":\"male\",\ \"birthday\":\"1956-05-12\",\ \"birthdate\":\"1956-07-12\",\ \"zoneinfo\":\"America/Los_Angeles\",\ \"locale\":\"en-US\",\ \"phone_number\":\"+1 (555) 555-5555\",\ \"phone_number_verified\":false,\ \"address\":{\ \"formatted\":\"1234 Hollywood Blvd., Los Angeles, CA 90210\",\ \"street_address\":\"1234 Hollywood Blvd.\",\ \"locality\":\"Los Angeles\",\ \"region\":\"CA\",\ \"postal_code\":\"90210\",\ \"country\":\"US\"\ },\ \"updated_at\":1311283970\ }"; let new_claims = CoreIdTokenClaims::new( IssuerUrl::new("https://server.example.com".to_string()).unwrap(), vec![Audience::new("s6BhdRkqt3".to_string())], Utc.timestamp_opt(1311281970, 0) .single() .expect("valid timestamp"), Utc.timestamp_opt(1311280970, 0) .single() .expect("valid timestamp"), StandardClaims { sub: SubjectIdentifier::new("24400320".to_string()), name: Some( vec![ (None, EndUserName::new("Homer Simpson".to_string())), ( Some(LanguageTag::new("es".to_string())), EndUserName::new("Jomer Simpson".to_string()), ), ] .into_iter() .collect(), ), given_name: Some( vec![ (None, EndUserGivenName::new("Homer".to_string())), ( Some(LanguageTag::new("es".to_string())), EndUserGivenName::new("Jomer".to_string()), ), ] .into_iter() .collect(), ), family_name: Some( vec![ (None, EndUserFamilyName::new("Simpson".to_string())), ( Some(LanguageTag::new("es".to_string())), EndUserFamilyName::new("Simpson".to_string()), ), ] .into_iter() .collect(), ), middle_name: Some( vec![ (None, EndUserMiddleName::new("Jay".to_string())), ( Some(LanguageTag::new("es".to_string())), EndUserMiddleName::new("Jay".to_string()), ), ] .into_iter() .collect(), ), nickname: Some( vec![ (None, EndUserNickname::new("Homer".to_string())), ( Some(LanguageTag::new("es".to_string())), EndUserNickname::new("Jomer".to_string()), ), ] .into_iter() .collect(), ), preferred_username: Some(EndUserUsername::new("homersimpson".to_string())), profile: Some( vec![ ( None, EndUserProfileUrl::new("https://example.com/profile?id=12345".to_string()), ), ( Some(LanguageTag::new("es".to_string())), EndUserProfileUrl::new( "https://example.com/profile?id=12345&lang=es".to_string(), ), ), ] .into_iter() .collect(), ), picture: Some( vec![ ( None, EndUserPictureUrl::new("https://example.com/avatar?id=12345".to_string()), ), ( Some(LanguageTag::new("es".to_string())), EndUserPictureUrl::new( "https://example.com/avatar?id=12345&lang=es".to_string(), ), ), ] .into_iter() .collect(), ), website: Some( vec![ ( None, EndUserWebsiteUrl::new("https://homersimpson.me".to_string()), ), ( Some(LanguageTag::new("es".to_string())), EndUserWebsiteUrl::new("https://homersimpson.me/?lang=es".to_string()), ), ] .into_iter() .collect(), ), email: Some(EndUserEmail::new("homer@homersimpson.me".to_string())), email_verified: Some(true), gender: Some(CoreGenderClaim::new("male".to_string())), birthday: Some(EndUserBirthday::new("1956-05-12".to_string())), birthdate: Some(EndUserBirthday::new("1956-07-12".to_string())), zoneinfo: Some(EndUserTimezone::new("America/Los_Angeles".to_string())), locale: Some(LanguageTag::new("en-US".to_string())), phone_number: Some(EndUserPhoneNumber::new("+1 (555) 555-5555".to_string())), phone_number_verified: Some(false), address: Some(AddressClaim { formatted: Some(FormattedAddress::new( "1234 Hollywood Blvd., Los Angeles, CA 90210".to_string(), )), street_address: Some(StreetAddress::new("1234 Hollywood Blvd.".to_string())), locality: Some(AddressLocality::new("Los Angeles".to_string())), region: Some(AddressRegion::new("CA".to_string())), postal_code: Some(AddressPostalCode::new("90210".to_string())), country: Some(AddressCountry::new("US".to_string())), }), updated_at: Some( Utc.timestamp_opt(1311283970, 0) .single() .expect("valid timestamp"), ), }, EmptyAdditionalClaims {}, ) .set_auth_time(Some( Utc.timestamp_opt(1311282970, 0) .single() .expect("valid timestamp"), )) .set_nonce(Some(Nonce::new("Zm9vYmFy".to_string()))) .set_auth_context_ref(Some(AuthenticationContextClass::new( "urn:mace:incommon:iap:silver".to_string(), ))) .set_auth_method_refs(Some(vec![ AuthenticationMethodReference::new("password".to_string()), AuthenticationMethodReference::new("totp".to_string()), ])) .set_authorized_party(Some(ClientId::new("dGhpc19jbGllbnQ".to_string()))) .set_access_token_hash(Some(AccessTokenHash::new( "_JPLB-GtkomFJxAOWKHPHQ".to_string(), ))) .set_code_hash(Some(AuthorizationCodeHash::new( "VpTQii5T_8rgwxA-Wtb2Bw".to_string(), ))); let claims: CoreIdTokenClaims = serde_json::from_str(claims_json).expect("failed to deserialize"); assert_eq!(claims, new_claims); assert_eq!(claims.issuer(), new_claims.issuer()); assert_eq!(claims.issuer().url(), new_claims.issuer().url()); assert_eq!(claims.audiences(), new_claims.audiences()); assert_eq!(claims.expiration(), new_claims.expiration()); assert_eq!(claims.issue_time(), new_claims.issue_time()); assert_eq!(claims.auth_time(), new_claims.auth_time()); assert_eq!( claims.nonce().unwrap().secret(), new_claims.nonce().unwrap().secret() ); assert_eq!(claims.auth_context_ref(), new_claims.auth_context_ref()); assert_eq!(claims.auth_method_refs(), new_claims.auth_method_refs()); assert_eq!(claims.authorized_party(), new_claims.authorized_party()); assert_eq!(claims.access_token_hash(), new_claims.access_token_hash()); assert_eq!(claims.code_hash(), new_claims.code_hash()); assert_eq!(*claims.additional_claims(), EmptyAdditionalClaims {}); assert_eq!(claims.subject(), new_claims.subject()); assert_eq!(claims.name(), new_claims.name()); assert_eq!(claims.given_name(), new_claims.given_name()); assert_eq!(claims.family_name(), new_claims.family_name()); assert_eq!(claims.middle_name(), new_claims.middle_name()); assert_eq!(claims.nickname(), new_claims.nickname()); assert_eq!(claims.preferred_username(), new_claims.preferred_username()); assert_eq!(claims.preferred_username(), new_claims.preferred_username()); assert_eq!(claims.profile(), new_claims.profile()); assert_eq!(claims.picture(), new_claims.picture()); assert_eq!(claims.website(), new_claims.website()); assert_eq!(claims.email(), new_claims.email()); assert_eq!(claims.email_verified(), new_claims.email_verified()); assert_eq!(claims.gender(), new_claims.gender()); assert_eq!(claims.birthday(), new_claims.birthday()); assert_eq!(claims.birthdate(), new_claims.birthdate()); assert_eq!(claims.zoneinfo(), new_claims.zoneinfo()); assert_eq!(claims.locale(), new_claims.locale()); assert_eq!(claims.phone_number(), new_claims.phone_number(),); assert_eq!( claims.phone_number_verified(), new_claims.phone_number_verified() ); assert_eq!(claims.address(), new_claims.address()); assert_eq!(claims.updated_at(), new_claims.updated_at()); let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); let claims_round_trip: CoreIdTokenClaims = serde_json::from_str(&serialized_claims).expect("failed to deserialize"); assert_eq!(claims, claims_round_trip); let serialized_new_claims = serde_json::to_string(&new_claims).expect("failed to serialize"); assert_eq!(serialized_new_claims, claims_json); } // See https://github.com/ramosbugs/openidconnect-rs/issues/23 #[test] #[cfg(feature = "accept-rfc3339-timestamps")] fn test_accept_rfc3339_timestamp() { let claims: CoreIdTokenClaims = serde_json::from_str( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": \"s6BhdRkqt3\", \"exp\": 1311281970, \"iat\": 1311280970, \"updated_at\": \"2021-12-22T02:10:37.000Z\" }", ) .expect("failed to deserialize"); assert_eq!( claims.updated_at(), Some( Utc.timestamp_opt(1640139037, 0) .single() .expect("valid timestamp") ) ); } #[test] fn test_unknown_claims_serde() { let expected_serialized_claims = "{\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\"],\ \"exp\":1311281970,\ \"iat\":1311280970,\ \"sub\":\"24400320\"\ }"; let claims: CoreIdTokenClaims = serde_json::from_str( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": \"s6BhdRkqt3\", \"exp\": 1311281970, \"iat\": 1311280970, \"some_other_field\":\"some_other_value\"\ }", ) .expect("failed to deserialize"); let serialized_claims = serde_json::to_string(&claims).expect("failed to serialize"); assert_eq!(serialized_claims, expected_serialized_claims); let claims_round_trip: CoreIdTokenClaims = serde_json::from_str(&serialized_claims).expect("failed to deserialize"); assert_eq!(claims, claims_round_trip); } #[test] fn test_audience() { let single_aud_str_claims = serde_json::from_str::( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": \"s6BhdRkqt3\", \"exp\": 1311281970, \"iat\": 1311280970 }", ) .expect("failed to deserialize"); assert_eq!( *single_aud_str_claims.audiences(), vec![Audience::new("s6BhdRkqt3".to_string())], ); // We always serialize aud as an array, which is valid according to the spec. assert_eq!( serde_json::to_string(&single_aud_str_claims).expect("failed to serialize"), "{\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\"],\ \"exp\":1311281970,\ \"iat\":1311280970,\ \"sub\":\"24400320\"\ }", ); let single_aud_vec_claims = serde_json::from_str::( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\"], \"exp\": 1311281970, \"iat\": 1311280970 }", ) .expect("failed to deserialize"); assert_eq!( *single_aud_vec_claims.audiences(), vec![Audience::new("s6BhdRkqt3".to_string())], ); assert_eq!( serde_json::to_string(&single_aud_vec_claims).expect("failed to serialize"), "{\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\"],\ \"exp\":1311281970,\ \"iat\":1311280970,\ \"sub\":\"24400320\"\ }", ); let multi_aud_claims = serde_json::from_str::( "{\ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\", \"aud2\"], \"exp\": 1311281970, \"iat\": 1311280970 }", ) .expect("failed to deserialize"); assert_eq!( *multi_aud_claims.audiences(), vec![ Audience::new("s6BhdRkqt3".to_string()), Audience::new("aud2".to_string()) ], ); assert_eq!( serde_json::to_string(&multi_aud_claims).expect("failed to serialize"), "{\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\",\"aud2\"],\ \"exp\":1311281970,\ \"iat\":1311280970,\ \"sub\":\"24400320\"\ }", ); } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] struct TestClaims { pub tfa_method: String, } impl AdditionalClaims for TestClaims {} #[test] fn test_additional_claims() { let claims = serde_json::from_str::>( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\"], \"exp\": 1311281970, \"iat\": 1311280970, \"tfa_method\": \"u2f\" }", ) .expect("failed to deserialize"); assert_eq!(claims.additional_claims().tfa_method, "u2f"); assert_eq!( serde_json::to_string(&claims).expect("failed to serialize"), "{\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\"],\ \"exp\":1311281970,\ \"iat\":1311280970,\ \"sub\":\"24400320\",\ \"tfa_method\":\"u2f\"\ }", ); serde_json::from_str::>( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\"], \"exp\": 1311281970, \"iat\": 1311280970 }", ) .expect_err("missing claim should fail to deserialize"); } #[derive(Debug, Deserialize, Serialize)] struct AllOtherClaims(HashMap); impl AdditionalClaims for AllOtherClaims {} #[test] fn test_catch_all_additional_claims() { let claims = serde_json::from_str::>( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\"], \"exp\": 1311281970, \"iat\": 1311280970, \"tfa_method\": \"u2f\", \"updated_at\": 1000 }", ) .expect("failed to deserialize"); assert_eq!(claims.additional_claims().0.len(), 1); assert_eq!(claims.additional_claims().0["tfa_method"], "u2f"); } #[test] fn test_audiences_claim() { let claims = serde_json::from_str::( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": \"s6BhdRkqt3\", \"exp\": 1311281970, \"iat\": 1311280970 }", ) .expect("failed to deserialize"); fn verify_audiences(audiences_claim: &A) { assert_eq!( (*audiences_claim).audiences(), Some(&vec![Audience::new("s6BhdRkqt3".to_string())]), ) } verify_audiences(&claims); verify_audiences(&&claims); } #[test] fn test_issuer_claim() { let claims = serde_json::from_str::( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": \"s6BhdRkqt3\", \"exp\": 1311281970, \"iat\": 1311280970 }", ) .expect("failed to deserialize"); fn verify_issuer(issuer_claim: &I) { assert_eq!( (*issuer_claim).issuer(), Some(&IssuerUrl::new("https://server.example.com".to_string()).unwrap()), ) } verify_issuer(&claims); verify_issuer(&&claims); } ================================================ FILE: src/jwt/mod.rs ================================================ use crate::{ JsonWebKey, JsonWebKeyId, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, PrivateSigningKey, SignatureVerificationError, SigningError, }; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use serde::de::{DeserializeOwned, Error as _, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::skip_serializing_none; use thiserror::Error; use std::fmt::Debug; use std::marker::PhantomData; #[cfg(test)] pub(crate) mod tests; new_type![ #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] JsonWebTokenContentType(String) ]; /// Error type used when normalizing [`JsonWebTokenType`] objects #[derive(Error, Debug)] #[error("Invalid JWT type: {typ}")] pub struct InvalidJsonWebTokenTypeError { typ: String, } new_type![ /// JSON Web Token type field (typ) /// /// This type stores the raw (deserialized) value. /// /// To compare two different JSON Web Token types, please use the normalized version via [`JsonWebTokenType::normalize`]. #[derive(Deserialize, Hash, Serialize)] JsonWebTokenType(String) impl { /// Expands a [`JsonWebTokenType`] and produces a [`NormalizedJsonWebTokenType`] according to RFC2045 and RFC7515. /// /// See [RFC 2045 section 5.1](https://tools.ietf.org/html/rfc2045#section-5.1) for the full Content-Type Header Field spec. /// See [RFC 7515 section 4.19](https://tools.ietf.org/html/rfc7515#section-4.1.9) for specific requirements of JSON Web Token Types. pub fn normalize(&self) -> Result { self.try_into() } } ]; /// Normalized JSON Web Token type field (typ) /// /// This type stores the normalized value of a [`JsonWebTokenType`]. /// To retrieve a normalized value according to RFC2045 and RFC7515 see [`JsonWebTokenType::normalize`] /// /// See [RFC 2045 section 5.1](https://tools.ietf.org/html/rfc2045#section-5.1) for the full Content-Type Header Field spec. /// See [RFC 7515 section 4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9) for specific requirements of JSON Web Token Types. /// /// It is recommended to instantiate `NormalizedJsonWebTokenType` objects via [`JsonWebTokenType`] and then call [`JsonWebTokenType::normalize`]. /// /// ```rust /// # use openidconnect::{NormalizedJsonWebTokenType, JsonWebTokenType}; /// let token_type = JsonWebTokenType::new("jwt+at".to_string()).normalize(); /// // normalized value looks like "application/jwt+at" /// # assert_eq!(*token_type.unwrap(), "application/jwt+at") /// ``` #[derive(Clone, Debug, Deserialize, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize)] pub struct NormalizedJsonWebTokenType(String); impl std::ops::Deref for NormalizedJsonWebTokenType { type Target = String; fn deref(&self) -> &String { &self.0 } } impl From for String { fn from(t: NormalizedJsonWebTokenType) -> String { t.0 } } impl From for JsonWebTokenType { fn from(t: NormalizedJsonWebTokenType) -> JsonWebTokenType { JsonWebTokenType::new(t.0) } } impl TryFrom<&JsonWebTokenType> for NormalizedJsonWebTokenType { type Error = InvalidJsonWebTokenTypeError; /// Normalizes a [`JsonWebTokenType`] and produces a [`NormalizedJsonWebTokenType`] according to RFC2045. /// /// See [RFC 2045 section 5.1](https://tools.ietf.org/html/rfc2045#section-5.1) for the full Content-Type Header Field spec. /// See [RFC 7515 section 4.19](https://tools.ietf.org/html/rfc7515#section-4.1.9) for specific requirements of JSON Web Token Types. fn try_from(t: &JsonWebTokenType) -> Result { let lowercase_jwt_type = t.0.to_lowercase(); if let Some(slash_location) = lowercase_jwt_type.find('/') { if let Some(semicolon_location) = lowercase_jwt_type.find(';') { // If '/' is not before ';' as then the MIME type is invalid // e.g. some;arg="1/2" is invalid, but application/some;arg=1 is valid // OR // If MIME type has not at least one character // OR // If MIME subtype has not at least one character if slash_location > semicolon_location || slash_location == 0 || slash_location.saturating_add(1) >= semicolon_location { Err(InvalidJsonWebTokenTypeError { typ: lowercase_jwt_type, }) } else { Ok(NormalizedJsonWebTokenType(lowercase_jwt_type)) } } else { Ok(NormalizedJsonWebTokenType(lowercase_jwt_type)) } } else { Ok(NormalizedJsonWebTokenType(format!( "application/{lowercase_jwt_type}" ))) } } } #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum JsonWebTokenAlgorithm where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { Encryption(JE), Signature(JS), /// No digital signature or MAC performed. /// /// # Security Warning /// /// This algorithm provides no security over the integrity of the JSON Web Token. Clients /// should be careful not to rely on unsigned JWT's for security purposes. See /// [Critical vulnerabilities in JSON Web Token libraries]( /// https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/) for /// further discussion. None, } impl<'de, JE, JS> Deserialize<'de> for JsonWebTokenAlgorithm where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let value: serde_json::Value = Deserialize::deserialize(deserializer)?; // TODO: get rid of this clone() (see below) let s: String = serde_json::from_value(value.clone()).map_err(D::Error::custom)?; // NB: These comparisons are case-sensitive. Section 4.1.1 of RFC 7515 states: "The "alg" // value is a case-sensitive ASCII string containing a StringOrURI value." if s == "none" { Ok(JsonWebTokenAlgorithm::None) // TODO: Figure out a way to deserialize the enums without giving up ownership } else if let Ok(val) = serde_json::from_value::(value.clone()) { Ok(JsonWebTokenAlgorithm::Encryption(val)) } else if let Ok(val) = serde_json::from_value::(value) { Ok(JsonWebTokenAlgorithm::Signature(val)) } else { Err(D::Error::custom(format!( "unrecognized JSON Web Algorithm `{}`", s ))) } } } impl Serialize for JsonWebTokenAlgorithm where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { fn serialize(&self, serializer: SE) -> Result where SE: Serializer, { match self { JsonWebTokenAlgorithm::Encryption(ref enc) => enc.serialize(serializer), JsonWebTokenAlgorithm::Signature(ref sig) => sig.serialize(serializer), JsonWebTokenAlgorithm::None => serializer.serialize_str("none"), } } } #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct JsonWebTokenHeader where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { #[serde( bound = "JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm" )] pub alg: JsonWebTokenAlgorithm, // Additional critical header parameters that must be understood by this implementation. Since // we don't understand any such extensions, we reject any JWT with this value present (the // spec specifically prohibits including public (standard) headers in this field). // See https://tools.ietf.org/html/rfc7515#section-4.1.11. pub crit: Option>, pub cty: Option, pub kid: Option, pub typ: Option, // Other JOSE header fields are omitted since the OpenID Connect spec specifically says that // the "x5u", "x5c", "jku", "jwk" header parameter fields SHOULD NOT be used. // See http://openid.net/specs/openid-connect-core-1_0-final.html#IDToken. } pub trait JsonWebTokenPayloadSerde

: Debug where P: Debug + DeserializeOwned + Serialize, { fn deserialize(payload: &[u8]) -> Result; fn serialize(payload: &P) -> Result; } #[derive(Clone, Debug, PartialEq, Eq)] pub struct JsonWebTokenJsonPayloadSerde; impl

JsonWebTokenPayloadSerde

for JsonWebTokenJsonPayloadSerde where P: Debug + DeserializeOwned + Serialize, { fn deserialize(payload: &[u8]) -> Result { serde_json::from_slice(payload) .map_err(|err| DE::custom(format!("Failed to parse payload JSON: {:?}", err))) } fn serialize(payload: &P) -> Result { serde_json::to_string(payload).map_err(Into::into) } } // Helper trait so that we can get borrowed payload when we have a reference to the JWT and owned // payload when we own the JWT. pub trait JsonWebTokenAccess where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, { type ReturnType; fn unverified_header(&self) -> &JsonWebTokenHeader; fn unverified_payload(self) -> Self::ReturnType; fn unverified_payload_ref(&self) -> &P; fn payload( self, signature_alg: &JS, key: &K, ) -> Result where K: JsonWebKey; fn signing_alg(&self) -> Result<&JS, SignatureVerificationError> { match self.unverified_header().alg { JsonWebTokenAlgorithm::Signature(ref signing_alg) => Ok(signing_alg), JsonWebTokenAlgorithm::Encryption(ref other) => { Err(SignatureVerificationError::UnsupportedAlg( serde_plain::to_string(other).unwrap_or_else(|err| { panic!( "encryption alg {:?} failed to serialize to a string: {}", other, err ) }), )) } // Section 2 of OpenID Connect Core 1.0 specifies that "ID Tokens MUST NOT use // none as the alg value unless the Response Type used returns no ID Token from // the Authorization Endpoint (such as when using the Authorization Code Flow) // and the Client explicitly requested the use of none at Registration time." // // While there's technically a use case where this is ok, we choose not to // support it for now to protect against accidental misuse. If demand arises, // we can figure out a API that mitigates the risk. JsonWebTokenAlgorithm::None => Err(SignatureVerificationError::NoSignature), } } } /// Error creating a JSON Web Token. #[derive(Debug, Error)] #[non_exhaustive] pub enum JsonWebTokenError { /// Failed to serialize JWT. #[error("Failed to serialize JWT")] SerializationError(#[source] serde_json::Error), /// Failed to sign JWT. #[error("Failed to sign JWT")] SigningError(#[source] SigningError), } #[derive(Clone, Debug, PartialEq, Eq)] pub struct JsonWebToken where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, { header: JsonWebTokenHeader, payload: P, signature: Vec, signing_input: String, _phantom: PhantomData, } impl JsonWebToken where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, { pub fn new(payload: P, signing_key: &SK, alg: &JS) -> Result where SK: PrivateSigningKey, ::VerificationKey: JsonWebKey, { let header = JsonWebTokenHeader:: { alg: JsonWebTokenAlgorithm::Signature(alg.clone()), crit: None, cty: None, kid: signing_key.as_verification_key().key_id().cloned(), typ: None, }; let header_json = serde_json::to_string(&header).map_err(JsonWebTokenError::SerializationError)?; let header_base64 = BASE64_URL_SAFE_NO_PAD.encode(header_json); let serialized_payload = S::serialize(&payload).map_err(JsonWebTokenError::SerializationError)?; let payload_base64 = BASE64_URL_SAFE_NO_PAD.encode(serialized_payload); let signing_input = format!("{}.{}", header_base64, payload_base64); let signature = signing_key .sign(alg, signing_input.as_bytes()) .map_err(JsonWebTokenError::SigningError)?; Ok(JsonWebToken { header, payload, signature, signing_input, _phantom: PhantomData, }) } } // Owned JWT. impl JsonWebTokenAccess for JsonWebToken where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, { type ReturnType = P; fn unverified_header(&self) -> &JsonWebTokenHeader { &self.header } fn unverified_payload(self) -> Self::ReturnType { self.payload } fn unverified_payload_ref(&self) -> &P { &self.payload } fn payload( self, signature_alg: &JS, key: &K, ) -> Result where K: JsonWebKey, { key.verify_signature( signature_alg, self.signing_input.as_bytes(), &self.signature, )?; Ok(self.payload) } } // Borrowed JWT. impl<'a, JE, JS, P, S> JsonWebTokenAccess for &'a JsonWebToken where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, { type ReturnType = &'a P; fn unverified_header(&self) -> &JsonWebTokenHeader { &self.header } fn unverified_payload(self) -> Self::ReturnType { &self.payload } fn unverified_payload_ref(&self) -> &P { &self.payload } fn payload( self, signature_alg: &JS, key: &K, ) -> Result where K: JsonWebKey, { key.verify_signature( signature_alg, self.signing_input.as_bytes(), &self.signature, )?; Ok(&self.payload) } } impl<'de, JE, JS, P, S> Deserialize<'de> for JsonWebToken where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct JsonWebTokenVisitor< JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, >(PhantomData<(JE, JS, P, S)>); impl<'de, JE, JS, P, S> Visitor<'de> for JsonWebTokenVisitor where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, { type Value = JsonWebToken; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("JsonWebToken") } fn visit_str(self, v: &str) -> Result where DE: serde::de::Error, { let raw_token = v.to_string(); let header: JsonWebTokenHeader; let payload: P; let signature; let signing_input; { let parts = raw_token.split('.').collect::>(); // NB: We avoid including the full payload encoding in the error output to avoid // clients potentially logging sensitive values. if parts.len() != 3 { return Err(DE::custom(format!( "Invalid JSON web token: found {} parts (expected 3)", parts.len() ))); } let header_json = crate::core::base64_url_safe_no_pad() .decode(parts[0]) .map_err(|err| { DE::custom(format!("Invalid base64url header encoding: {:?}", err)) })?; header = serde_json::from_slice(&header_json).map_err(|err| { DE::custom(format!("Failed to parse header JSON: {:?}", err)) })?; let raw_payload = crate::core::base64_url_safe_no_pad() .decode(parts[1]) .map_err(|err| { DE::custom(format!("Invalid base64url payload encoding: {:?}", err)) })?; payload = S::deserialize::(&raw_payload)?; signature = crate::core::base64_url_safe_no_pad() .decode(parts[2]) .map_err(|err| { DE::custom(format!("Invalid base64url signature encoding: {:?}", err)) })?; signing_input = format!("{}.{}", parts[0], parts[1]); } Ok(JsonWebToken { header, payload, signature, signing_input, _phantom: PhantomData, }) } } deserializer.deserialize_str(JsonWebTokenVisitor(PhantomData)) } } impl Serialize for JsonWebToken where JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, P: Debug + DeserializeOwned + Serialize, S: JsonWebTokenPayloadSerde

, { fn serialize(&self, serializer: SE) -> Result where SE: Serializer, { let signature_base64 = BASE64_URL_SAFE_NO_PAD.encode(&self.signature); serializer.serialize_str(&format!("{}.{}", self.signing_input, signature_base64)) } } ================================================ FILE: src/jwt/tests.rs ================================================ use crate::core::{ CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, }; use crate::jwt::{ InvalidJsonWebTokenTypeError, JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenJsonPayloadSerde, JsonWebTokenPayloadSerde, }; use crate::{JsonWebKeyId, JsonWebTokenType}; use serde::{Deserialize, Serialize}; use std::string::ToString; type CoreAlgorithm = JsonWebTokenAlgorithm; pub const TEST_JWT: &str = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.SXTigJlzIGEgZ\ GFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGU\ gcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlc\ mUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4.MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e\ 5CZ5NlKtainoFmKZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJK3l\ fWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV\ 0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41\ Axf_fcIe8u9ipH84ogoree7vjbU5y18kDquDg"; const TEST_JWT_PAYLOAD: &str = "It\u{2019}s a dangerous business, Frodo, going out your \ door. You step onto the road, and if you don't keep your feet, \ there\u{2019}s no knowing where you might be swept off \ to."; pub const TEST_RSA_PUB_KEY: &str = "{ \"kty\": \"RSA\", \"kid\": \"bilbo.baggins@hobbiton.example\", \"use\": \"sig\", \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ HdrNP5zw\", \"e\": \"AQAB\" }"; pub const TEST_ED_PUB_KEY_ED25519: &str = r#"{ "kty": "OKP", "use": "sig", "alg": "EdDSA", "crv": "Ed25519", "x": "sfliRRhciU_d5qsuC5Vcydi-t8bRfxTg_4qulVatW4A" }"#; pub const TEST_EC_PUB_KEY_P256: &str = r#"{ "kty": "EC", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "crv": "P-256", "x": "t6PHivOTggpaX9lkMkis2p8kMhy-CktJAFTz6atReZw", "y": "ODobXupKlD0DeM1yRd7bX4XFNBO1HOgCT1UCu0KY3lc" }"#; pub const TEST_EC_PUB_KEY_P384: &str = r#"{ "kty": "EC", "kid": "bilbo.baggins@hobbiton.example", "use": "sig", "crv" : "P-384", "x": "9ywsUbxX59kJXFRiWHcx97wRKNiF8Hc9F5wI08n8h2ek_qAl0veEc36k1Qz6KLiL", "y": "6PWlqjRbaV7V8ohDscM243IneuLZmxDGLiGNA1w69fQhEDsvZtKLUQ5KiHLgR3op" }"#; // This is the PEM form of the test private key from: // https://tools.ietf.org/html/rfc7520#section-3.4 pub const TEST_RSA_PRIV_KEY: &str = "-----BEGIN RSA PRIVATE KEY-----\n\ MIIEowIBAAKCAQEAn4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8/KuKPEHLd4\n\ rHVTeT+O+XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz/AJmSCpMaJMRBSFKrKb2wqVwG\n\ U/NsYOYL+QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj+oBHqFEHYpP\n\ e7Tpe+OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzw\n\ OHrtIQbS0FVbb9k3+tVTU4fg/3L/vniUFAKwuCLqKnS2BYwdq/mzSnbLY7h/qixo\n\ R7jig3//kRhuaxwUkRz5iaiQkqgc5gHdrNP5zwIDAQABAoIBAG1lAvQfhBUSKPJK\n\ Rn4dGbshj7zDSr2FjbQf4pIh/ZNtHk/jtavyO/HomZKV8V0NFExLNi7DUUvvLiW7\n\ 0PgNYq5MDEjJCtSd10xoHa4QpLvYEZXWO7DQPwCmRofkOutf+NqyDS0QnvFvp2d+\n\ Lov6jn5C5yvUFgw6qWiLAPmzMFlkgxbtjFAWMJB0zBMy2BqjntOJ6KnqtYRMQUxw\n\ TgXZDF4rhYVKtQVOpfg6hIlsaoPNrF7dofizJ099OOgDmCaEYqM++bUlEHxgrIVk\n\ wZz+bg43dfJCocr9O5YX0iXaz3TOT5cpdtYbBX+C/5hwrqBWru4HbD3xz8cY1TnD\n\ qQa0M8ECgYEA3Slxg/DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex/fp7AZ/9\n\ nRaO7HX/+SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr/WCsmGpeNqQn\n\ ev1T7IyEsnh8UMt+n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0kCgYEAuKE2\n\ dh+cTf6ERF4k4e/jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR/cu0Dm1MZwW\n\ mtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoB\n\ vyY898EXvRD+hdqRxHlSqAZ192zB3pVFJ0s7pFcCgYAHw9W9eS8muPYv4ZhDu/fL\n\ 2vorDmD1JqFcHCxZTOnX1NWWAj5hXzmrU0hvWvFC0P4ixddHf5Nqd6+5E9G3k4E5\n\ 2IwZCnylu3bqCWNh8pT8T3Gf5FQsfPT5530T2BcsoPhUaeCnP499D+rb2mTnFYeg\n\ mnTT1B/Ue8KGLFFfn16GKQKBgAiw5gxnbocpXPaO6/OKxFFZ+6c0OjxfN2PogWce\n\ TU/k6ZzmShdaRKwDFXisxRJeNQ5Rx6qgS0jNFtbDhW8E8WFmQ5urCOqIOYk28EBi\n\ At4JySm4v+5P7yYBh8B8YD2l9j57z/s8hJAxEbn/q8uHP2ddQqvQKgtsni+pHSk9\n\ XGBfAoGBANz4qr10DdM8DHhPrAb2YItvPVz/VwkBd1Vqj8zCpyIEKe/07oKOvjWQ\n\ SgkLDH9x2hBgY01SbP43CvPk0V72invu2TGkI/FXwXWJLLG7tDSgw4YyfhrYrHmg\n\ 1Vre3XB9HH8MYBVB6UIexaAq4xSeoemRKTBesZro7OKjKT8/GmiO\n\ -----END RSA PRIVATE KEY-----"; #[test] fn test_jwt_algorithm_deserialization() { assert_eq!( serde_json::from_str::("\"A128CBC-HS256\"").expect("failed to deserialize"), JsonWebTokenAlgorithm::Encryption(CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256), ); assert_eq!( serde_json::from_str::("\"A128GCM\"").expect("failed to deserialize"), JsonWebTokenAlgorithm::Encryption(CoreJweContentEncryptionAlgorithm::Aes128Gcm), ); assert_eq!( serde_json::from_str::("\"HS256\"").expect("failed to deserialize"), JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::HmacSha256), ); assert_eq!( serde_json::from_str::("\"RS256\"").expect("failed to deserialize"), JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256), ); assert_eq!( serde_json::from_str::("\"none\"").expect("failed to deserialize"), JsonWebTokenAlgorithm::None, ); serde_json::from_str::("\"invalid\"") .expect_err("deserialization should have failed"); } #[test] fn test_jwt_algorithm_serialization() { assert_eq!( serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 )) .expect("failed to serialize"), "\"A128CBC-HS256\"", ); assert_eq!( serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( CoreJweContentEncryptionAlgorithm::Aes128Gcm )) .expect("failed to serialize"), "\"A128GCM\"", ); assert_eq!( serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( CoreJwsSigningAlgorithm::HmacSha256 )) .expect("failed to serialize"), "\"HS256\"", ); assert_eq!( serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 )) .expect("failed to serialize"), "\"RS256\"", ); assert_eq!( serde_json::to_string::(&JsonWebTokenAlgorithm::None) .expect("failed to serialize"), "\"none\"", ); } #[derive(Clone, Debug)] pub struct JsonWebTokenStringPayloadSerde; impl JsonWebTokenPayloadSerde for JsonWebTokenStringPayloadSerde { fn deserialize(payload: &[u8]) -> Result { Ok(String::from_utf8(payload.to_owned()).unwrap()) } fn serialize(payload: &String) -> Result { Ok(payload.to_string()) } } #[test] fn test_jwt_basic() { fn verify_jwt(jwt_access: A, key: &CoreJsonWebKey, expected_payload: &str) where A: JsonWebTokenAccess, A::ReturnType: ToString, { { let header = jwt_access.unverified_header(); assert_eq!( header.alg, JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256) ); assert_eq!(header.crit, None); assert_eq!(header.cty, None); assert_eq!( header.kid, Some(JsonWebKeyId::new( "bilbo.baggins@hobbiton.example".to_string() )) ); assert_eq!(header.typ, None); } assert_eq!(jwt_access.unverified_payload_ref(), expected_payload); assert_eq!( jwt_access .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, key) .expect("failed to validate payload") .to_string(), expected_payload ); } let key: CoreJsonWebKey = serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); let jwt: JsonWebToken< CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, String, JsonWebTokenStringPayloadSerde, > = serde_json::from_value(serde_json::Value::String(TEST_JWT.to_string())) .expect("failed to deserialize"); assert_eq!( serde_json::to_value(&jwt).expect("failed to serialize"), serde_json::Value::String(TEST_JWT.to_string()) ); verify_jwt(&jwt, &key, TEST_JWT_PAYLOAD); assert_eq!((&jwt).unverified_payload(), TEST_JWT_PAYLOAD); verify_jwt(jwt, &key, TEST_JWT_PAYLOAD); } #[test] fn test_new_jwt() { let signing_key = CoreRsaPrivateSigningKey::from_pem( TEST_RSA_PRIV_KEY, Some(JsonWebKeyId::new( "bilbo.baggins@hobbiton.example".to_string(), )), ) .unwrap(); let new_jwt = JsonWebToken::< CoreJweContentEncryptionAlgorithm, _, _, JsonWebTokenStringPayloadSerde, >::new( TEST_JWT_PAYLOAD.to_owned(), &signing_key, &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, ) .unwrap(); assert_eq!( serde_json::to_value(new_jwt).expect("failed to serialize"), serde_json::Value::String(TEST_JWT.to_string()) ); } #[test] fn test_invalid_signature() { let corrupted_jwt_str = TEST_JWT .to_string() .chars() .take(TEST_JWT.len() - 1) .collect::() + "f"; let jwt: JsonWebToken< CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, String, JsonWebTokenStringPayloadSerde, > = serde_json::from_value(serde_json::Value::String(corrupted_jwt_str)) .expect("failed to deserialize"); let key: CoreJsonWebKey = serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); // JsonWebTokenAccess for reference. (&jwt) .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) .expect_err("signature verification should have failed"); // JsonWebTokenAccess for owned value. jwt.payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) .expect_err("signature verification should have failed"); } #[test] fn test_invalid_deserialization() { #[derive(Debug, Deserialize, Serialize)] struct TestPayload { foo: String, } fn expect_deserialization_err>(jwt_str: I, pattern: &str) { let err = serde_json::from_value::< JsonWebToken< CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, TestPayload, JsonWebTokenJsonPayloadSerde, >, >(serde_json::Value::String(jwt_str.into())) .expect_err("deserialization should have failed"); assert!( err.to_string().contains(pattern), "Error `{}` must contain string `{}`", err, pattern, ); } // Too many dots expect_deserialization_err("a.b.c.d", "found 4 parts (expected 3)"); // Invalid header base64 expect_deserialization_err("a!.b.c", "Invalid base64url header encoding"); // Invalid header utf-8 (after base64 decoding) expect_deserialization_err("gA.b.c", "Error(\"expected value\", line: 1, column: 1)"); // Invalid header JSON expect_deserialization_err("bm90X2pzb24.b.c", "Failed to parse header JSON"); let valid_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9"; // Invalid payload base64 expect_deserialization_err( format!("{}.b!.c", valid_header), "Invalid base64url payload encoding", ); // Invalid payload utf-8 (after base64 decoding) expect_deserialization_err( format!("{}.gA.c", valid_header), "Error(\"expected value\", line: 1, column: 1)", ); // Invalid payload JSON expect_deserialization_err( format!("{}.bm90X2pzb24.c", valid_header), "Failed to parse payload JSON", ); let valid_body = "eyJmb28iOiAiYmFyIn0"; // Invalid signature base64 expect_deserialization_err( format!("{}.{}.c!", valid_header, valid_body), "Invalid base64url signature encoding", ); let deserialized = serde_json::from_value::< JsonWebToken< CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, TestPayload, JsonWebTokenJsonPayloadSerde, >, >(serde_json::Value::String(format!( "{}.{}.e2FiY30", valid_header, valid_body ))) .expect("failed to deserialize"); assert_eq!(deserialized.unverified_payload().foo, "bar"); } #[test] fn test_json_web_token_type_normalization() { fn assert_token_type_normalization( jwt_type_string: &str, expected_normalized_jwt_type_string: &str, ) -> Result<(), InvalidJsonWebTokenTypeError> { let jwt_type = JsonWebTokenType::new(jwt_type_string.to_string()); let normalized_jwt_type = jwt_type.normalize()?; assert_eq!(*normalized_jwt_type, expected_normalized_jwt_type_string); Ok(()) } assert_token_type_normalization("jwt", "application/jwt").unwrap(); assert_token_type_normalization("jwt;arg=some", "application/jwt;arg=some").unwrap(); assert!(assert_token_type_normalization("jwt;arg=some/other", "").is_err()); assert!(assert_token_type_normalization("/jwt;arg=some/other", "").is_err()); assert!(assert_token_type_normalization("application/;arg=some/other", "").is_err()); assert_token_type_normalization("application/jwt", "application/jwt").unwrap(); assert_token_type_normalization( "application/jwt;arg=some/other", "application/jwt;arg=some/other", ) .unwrap(); assert_token_type_normalization("special/type", "special/type").unwrap(); assert_token_type_normalization("special/type;arg=some", "special/type;arg=some").unwrap(); assert_token_type_normalization("s/t;arg=some/o", "s/t;arg=some/o").unwrap(); } ================================================ FILE: src/lib.rs ================================================ #![warn(missing_docs)] #![allow(clippy::unreadable_literal, clippy::type_complexity)] #![cfg_attr(test, allow(clippy::cognitive_complexity))] //! //! [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) library. //! //! This library provides extensible, strongly-typed interfaces for the OpenID Connect protocol. //! For convenience, the [`core`] module provides type aliases for common usage that adheres to the //! [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) spec. Users of //! this crate may define their own extensions and custom type parameters in lieu of using the //! [`core`] module. //! //! # Contents //! * [Importing `openidconnect`: selecting an HTTP client interface](#importing-openidconnect-selecting-an-http-client-interface) //! * [OpenID Connect Relying Party (Client) Interface](#openid-connect-relying-party-client-interface) //! * [Examples](#examples) //! * [Getting started: Authorization Code Grant w/ PKCE](#getting-started-authorization-code-grant-w-pkce) //! * [OpenID Connect Provider (Server) Interface](#openid-connect-provider-server-interface) //! * [OpenID Connect Discovery document](#openid-connect-discovery-document) //! * [OpenID Connect Discovery JSON Web Key Set](#openid-connect-discovery-json-web-key-set) //! * [OpenID Connect ID Token](#openid-connect-id-token) //! * [Asynchronous API](#asynchronous-api) //! //! # Importing `openidconnect`: selecting an HTTP client interface //! //! //! This library offers a flexible HTTP client interface with two modes: //! * **Synchronous (blocking)** //! //! NOTE: Be careful not to use a blocking HTTP client within `async` Rust code, which may panic //! or cause other issues. The //! [`tokio::task::spawn_blocking`](https://docs.rs/tokio/latest/tokio/task/fn.spawn_blocking.html) //! function may be useful in this situation. //! * **Asynchronous** //! //! ## Security Warning //! //! To prevent //! [SSRF](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) //! vulnerabilities, be sure to configure the HTTP client **not to follow redirects**. For example, //! use [`redirect::Policy::none`](reqwest::redirect::Policy::none) when using //! [`reqwest`], or [`redirects(0)`](ureq::AgentBuilder::redirects) when using [`ureq`]. //! //! ## HTTP Clients //! //! For the HTTP client modes described above, the following HTTP client implementations can be //! used: //! * **[`reqwest`]** //! //! The `reqwest` HTTP client supports both the synchronous and asynchronous modes and is enabled //! by default. //! //! Synchronous client: [`reqwest::blocking::Client`] //! (requires the `reqwest-blocking` feature flag) //! //! Asynchronous client: [`reqwest::Client`] (requires either //! the `reqwest` or `reqwest-blocking` feature flags) //! //! * **[`curl`]** //! //! The `curl` HTTP client only supports the synchronous HTTP client mode and can be enabled in //! `Cargo.toml` via the `curl` feature flag. //! //! Synchronous client: [`CurlHttpClient`] //! //! * **[`ureq`]** //! //! The `ureq` HTTP client is a simple HTTP client with minimal dependencies. It only supports //! the synchronous HTTP client mode and can be enabled in `Cargo.toml` via the `ureq` feature //! flag. //! //! Synchronous client: [`ureq::Agent`] //! //! * **Custom** //! //! In addition to the clients above, users may define their own HTTP clients, which must accept //! an [`HttpRequest`] and return an [`HttpResponse`] or error. Users writing their own clients //! may wish to disable the default `reqwest` dependency by specifying //! `default-features = false` in `Cargo.toml` (replacing `...` with the desired version of this //! crate): //! ```toml //! openidconnect = { version = "...", default-features = false } //! ``` //! //! Synchronous HTTP clients should implement the [`SyncHttpClient`] trait, which is //! automatically implemented for any function/closure that implements: //! ```rust,ignore //! Fn(HttpRequest) -> Result //! where //! E: std::error::Error + 'static //! ``` //! //! Asynchronous HTTP clients should implement the [`AsyncHttpClient`] trait, which is //! automatically implemented for any function/closure that implements: //! ```rust,ignore //! Fn(HttpRequest) -> F //! where //! E: std::error::Error + 'static, //! F: Future>, //! ``` //! //! # Comparing secrets securely //! //! OpenID Connect flows require comparing secrets received from providers. To do so securely //! while avoiding [timing side-channels](https://en.wikipedia.org/wiki/Timing_attack), the //! comparison must be done in constant time, either using a constant-time crate such as //! [`constant_time_eq`](https://crates.io/crates/constant_time_eq) (which could break if a future //! compiler version decides to be overly smart //! about its optimizations), or by first computing a cryptographically-secure hash (e.g., SHA-256) //! of both values and then comparing the hashes using `==`. //! //! The `timing-resistant-secret-traits` feature flag adds a safe (but comparatively expensive) //! [`PartialEq`] implementation to the secret types. Timing side-channels are why [`PartialEq`] is //! not auto-derived for this crate's secret types, and the lack of [`PartialEq`] is intended to //! prompt users to think more carefully about these comparisons. //! //! # OpenID Connect Relying Party (Client) Interface //! //! The [`Client`] struct provides the OpenID Connect Relying Party interface. The most common //! usage is provided by the [`core::CoreClient`] type alias. //! //! ## Examples //! //! * [Google](https://github.com/ramosbugs/openidconnect-rs/tree/main/examples/google.rs) //! //! ## Getting started: Authorization Code Grant w/ PKCE //! //! This is the most common OIDC/OAuth2 flow. PKCE is recommended whenever the client has no //! client secret or has a client secret that cannot remain confidential (e.g., native, mobile, or //! client-side web applications). //! //! ### Example //! //! ```rust,no_run //! use anyhow::anyhow; //! use openidconnect::{ //! AccessTokenHash, //! AuthenticationFlow, //! AuthorizationCode, //! ClientId, //! ClientSecret, //! CsrfToken, //! IssuerUrl, //! Nonce, //! OAuth2TokenResponse, //! PkceCodeChallenge, //! RedirectUrl, //! Scope, //! TokenResponse, //! }; //! use openidconnect::core::{ //! CoreAuthenticationFlow, //! CoreClient, //! CoreProviderMetadata, //! CoreResponseType, //! CoreUserInfoClaims, //! }; //! # #[cfg(feature = "reqwest-blocking")] //! use openidconnect::reqwest; //! use url::Url; //! //! # #[cfg(feature = "reqwest-blocking")] //! # fn err_wrapper() -> Result<(), anyhow::Error> { //! let http_client = reqwest::blocking::ClientBuilder::new() //! // Following redirects opens the client up to SSRF vulnerabilities. //! .redirect(reqwest::redirect::Policy::none()) //! .build() //! .expect("Client should build"); //! //! // Use OpenID Connect Discovery to fetch the provider metadata. //! let provider_metadata = CoreProviderMetadata::discover( //! &IssuerUrl::new("https://accounts.example.com".to_string())?, //! &http_client, //! )?; //! //! // Create an OpenID Connect client by specifying the client ID, client secret, authorization URL //! // and token URL. //! let client = //! CoreClient::from_provider_metadata( //! provider_metadata, //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! ) //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?); //! //! // Generate a PKCE challenge. //! let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); //! //! // Generate the full authorization URL. //! let (auth_url, csrf_token, nonce) = client //! .authorize_url( //! CoreAuthenticationFlow::AuthorizationCode, //! CsrfToken::new_random, //! Nonce::new_random, //! ) //! // Set the desired scopes. //! .add_scope(Scope::new("read".to_string())) //! .add_scope(Scope::new("write".to_string())) //! // Set the PKCE code challenge. //! .set_pkce_challenge(pkce_challenge) //! .url(); //! //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. //! println!("Browse to: {}", auth_url); //! //! // Once the user has been redirected to the redirect URL, you'll have access to the //! // authorization code. For security reasons, your code should verify that the `state` //! // parameter returned by the server matches `csrf_state`. //! //! // Now you can exchange it for an access token and ID token. //! let token_response = //! client //! .exchange_code(AuthorizationCode::new("some authorization code".to_string()))? //! // Set the PKCE code verifier. //! .set_pkce_verifier(pkce_verifier) //! .request(&http_client)?; //! //! // Extract the ID token claims after verifying its authenticity and nonce. //! let id_token = token_response //! .id_token() //! .ok_or_else(|| anyhow!("Server did not return an ID token"))?; //! let id_token_verifier = client.id_token_verifier(); //! let claims = id_token.claims(&id_token_verifier, &nonce)?; //! //! // Verify the access token hash to ensure that the access token hasn't been substituted for //! // another user's. //! if let Some(expected_access_token_hash) = claims.access_token_hash() { //! let actual_access_token_hash = AccessTokenHash::from_token( //! token_response.access_token(), //! id_token.signing_alg()?, //! id_token.signing_key(&id_token_verifier)?, //! )?; //! if actual_access_token_hash != *expected_access_token_hash { //! return Err(anyhow!("Invalid access token")); //! } //! } //! //! // The authenticated user's identity is now available. See the IdTokenClaims struct for a //! // complete listing of the available claims. //! println!( //! "User {} with e-mail address {} has authenticated successfully", //! claims.subject().as_str(), //! claims.email().map(|email| email.as_str()).unwrap_or(""), //! ); //! //! // If available, we can use the user info endpoint to request additional information. //! //! // The user_info request uses the AccessToken returned in the token response. To parse custom //! // claims, use UserInfoClaims directly (with the desired type parameters) rather than using the //! // CoreUserInfoClaims type alias. //! let userinfo: CoreUserInfoClaims = client //! .user_info(token_response.access_token().to_owned(), None)? //! .request(&http_client) //! .map_err(|err| anyhow!("Failed requesting user info: {}", err))?; //! //! // See the OAuth2TokenResponse trait for a listing of other available fields such as //! // access_token() and refresh_token(). //! //! # Ok(()) //! # } //! ``` //! //! # OpenID Connect Provider (Server) Interface //! //! This library does not implement a complete OpenID Connect Provider, which requires //! functionality such as credential and session management. However, it does provide //! strongly-typed interfaces for parsing and building OpenID Connect protocol messages. //! //! ## OpenID Connect Discovery document //! //! The [`ProviderMetadata`] struct implements the //! [OpenID Connect Discovery document](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig). //! This data structure should be serialized to JSON and served via the //! `GET .well-known/openid-configuration` path relative to your provider's issuer URL. //! //! ### Example //! //! ```rust,no_run //! use openidconnect::{ //! AuthUrl, //! EmptyAdditionalProviderMetadata, //! IssuerUrl, //! JsonWebKeySetUrl, //! ResponseTypes, //! Scope, //! TokenUrl, //! UserInfoUrl, //! }; //! use openidconnect::core::{ //! CoreClaimName, //! CoreJwsSigningAlgorithm, //! CoreProviderMetadata, //! CoreResponseType, //! CoreSubjectIdentifierType //! }; //! use url::Url; //! //! # fn err_wrapper() -> Result { //! let provider_metadata = CoreProviderMetadata::new( //! // Parameters required by the OpenID Connect Discovery spec. //! IssuerUrl::new("https://accounts.example.com".to_string())?, //! AuthUrl::new("https://accounts.example.com/authorize".to_string())?, //! // Use the JsonWebKeySet struct to serve the JWK Set at this URL. //! JsonWebKeySetUrl::new("https://accounts.example.com/jwk".to_string())?, //! // Supported response types (flows). //! vec![ //! // Recommended: support the code flow. //! ResponseTypes::new(vec![CoreResponseType::Code]), //! // Optional: support the implicit flow. //! ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken]) //! // Other flows including hybrid flows may also be specified here. //! ], //! // For user privacy, the Pairwise subject identifier type is preferred. This prevents //! // distinct relying parties (clients) from knowing whether their users represent the same //! // real identities. This identifier type is only useful for relying parties that don't //! // receive the 'email', 'profile' or other personally-identifying scopes. //! // The Public subject identifier type is also supported. //! vec![CoreSubjectIdentifierType::Pairwise], //! // Support the RS256 signature algorithm. //! vec![CoreJwsSigningAlgorithm::RsaSsaPssSha256], //! // OpenID Connect Providers may supply custom metadata by providing a struct that //! // implements the AdditionalProviderMetadata trait. This requires manually using the //! // generic ProviderMetadata struct rather than the CoreProviderMetadata type alias, //! // however. //! EmptyAdditionalProviderMetadata {}, //! ) //! // Specify the token endpoint (required for the code flow). //! .set_token_endpoint(Some(TokenUrl::new("https://accounts.example.com/token".to_string())?)) //! // Recommended: support the user info endpoint. //! .set_userinfo_endpoint( //! Some(UserInfoUrl::new("https://accounts.example.com/userinfo".to_string())?) //! ) //! // Recommended: specify the supported scopes. //! .set_scopes_supported(Some(vec![ //! Scope::new("openid".to_string()), //! Scope::new("email".to_string()), //! Scope::new("profile".to_string()), //! ])) //! // Recommended: specify the supported ID token claims. //! .set_claims_supported(Some(vec![ //! // Providers may also define an enum instead of using CoreClaimName. //! CoreClaimName::new("sub".to_string()), //! CoreClaimName::new("aud".to_string()), //! CoreClaimName::new("email".to_string()), //! CoreClaimName::new("email_verified".to_string()), //! CoreClaimName::new("exp".to_string()), //! CoreClaimName::new("iat".to_string()), //! CoreClaimName::new("iss".to_string()), //! CoreClaimName::new("name".to_string()), //! CoreClaimName::new("given_name".to_string()), //! CoreClaimName::new("family_name".to_string()), //! CoreClaimName::new("picture".to_string()), //! CoreClaimName::new("locale".to_string()), //! ])); //! //! serde_json::to_string(&provider_metadata).map_err(From::from) //! # } //! ``` //! //! ## OpenID Connect Discovery JSON Web Key Set //! //! The JSON Web Key Set (JWKS) provides the public keys that relying parties (clients) use to //! verify the authenticity of ID tokens returned by this OpenID Connect Provider. The //! [`JsonWebKeySet`] data structure should be serialized as JSON and served at the URL specified //! in the `jwks_uri` field of the [`ProviderMetadata`] returned in the OpenID Connect Discovery //! document. //! //! ### Example //! //! ```rust,no_run //! use openidconnect::{JsonWebKeyId, PrivateSigningKey}; //! use openidconnect::core::{CoreJsonWebKey, CoreJsonWebKeySet, CoreRsaPrivateSigningKey}; //! //! # fn err_wrapper() -> Result { //! # let rsa_pem = ""; //! let jwks = CoreJsonWebKeySet::new( //! vec![ //! // RSA keys may also be constructed directly using CoreJsonWebKey::new_rsa(). Providers //! // aiming to support other key types may provide their own implementation of the //! // JsonWebKey trait or submit a PR to add the desired support to this crate. //! CoreRsaPrivateSigningKey::from_pem( //! &rsa_pem, //! Some(JsonWebKeyId::new("key1".to_string())) //! ) //! .expect("Invalid RSA private key") //! .as_verification_key() //! ] //! ); //! //! serde_json::to_string(&jwks).map_err(From::from) //! # } //! ``` //! //! ## OpenID Connect ID Token //! //! The [`IdToken::new`] method is used for signing ID token claims, which can then be returned //! from the token endpoint as part of the [`StandardTokenResponse`] struct //! (or [`core::CoreTokenResponse`] type alias). The ID token can also be serialized to a string //! using the `IdToken::to_string` method and returned directly from the authorization endpoint //! when the implicit flow or certain hybrid flows are used. Note that in these flows, ID tokens //! must only be returned in the URL fragment, and never as a query parameter. //! //! The ID token contains a combination of the //! [OpenID Connect Standard Claims](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) //! (see [`StandardClaims`]) and claims specific to the //! [OpenID Connect ID Token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) //! (see [`IdTokenClaims`]). //! //! ### Example //! //! ```rust,no_run //! use chrono::{Duration, Utc}; //! use openidconnect::{ //! AccessToken, //! Audience, //! EmptyAdditionalClaims, //! EmptyExtraTokenFields, //! EndUserEmail, //! IssuerUrl, //! JsonWebKeyId, //! StandardClaims, //! SubjectIdentifier, //! }; //! use openidconnect::core::{ //! CoreIdToken, //! CoreIdTokenClaims, //! CoreIdTokenFields, //! CoreJwsSigningAlgorithm, //! CoreRsaPrivateSigningKey, //! CoreTokenResponse, //! CoreTokenType, //! }; //! //! # fn err_wrapper() -> Result { //! # let rsa_pem = ""; //! # let access_token = AccessToken::new("".to_string()); //! let id_token = CoreIdToken::new( //! CoreIdTokenClaims::new( //! // Specify the issuer URL for the OpenID Connect Provider. //! IssuerUrl::new("https://accounts.example.com".to_string())?, //! // The audience is usually a single entry with the client ID of the client for whom //! // the ID token is intended. This is a required claim. //! vec![Audience::new("client-id-123".to_string())], //! // The ID token expiration is usually much shorter than that of the access or refresh //! // tokens issued to clients. //! Utc::now() + Duration::seconds(300), //! // The issue time is usually the current time. //! Utc::now(), //! // Set the standard claims defined by the OpenID Connect Core spec. //! StandardClaims::new( //! // Stable subject identifiers are recommended in place of e-mail addresses or other //! // potentially unstable identifiers. This is the only required claim. //! SubjectIdentifier::new("5f83e0ca-2b8e-4e8c-ba0a-f80fe9bc3632".to_string()) //! ) //! // Optional: specify the user's e-mail address. This should only be provided if the //! // client has been granted the 'profile' or 'email' scopes. //! .set_email(Some(EndUserEmail::new("bob@example.com".to_string()))) //! // Optional: specify whether the provider has verified the user's e-mail address. //! .set_email_verified(Some(true)), //! // OpenID Connect Providers may supply custom claims by providing a struct that //! // implements the AdditionalClaims trait. This requires manually using the //! // generic IdTokenClaims struct rather than the CoreIdTokenClaims type alias, //! // however. //! EmptyAdditionalClaims {}, //! ), //! // The private key used for signing the ID token. For confidential clients (those able //! // to maintain a client secret), a CoreHmacKey can also be used, in conjunction //! // with one of the CoreJwsSigningAlgorithm::HmacSha* signing algorithms. When using an //! // HMAC-based signing algorithm, the UTF-8 representation of the client secret should //! // be used as the HMAC key. //! &CoreRsaPrivateSigningKey::from_pem( //! &rsa_pem, //! Some(JsonWebKeyId::new("key1".to_string())) //! ) //! .expect("Invalid RSA private key"), //! // Uses the RS256 signature algorithm. This crate supports any RS*, PS*, or HS* //! // signature algorithm. //! CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, //! // When returning the ID token alongside an access token (e.g., in the Authorization Code //! // flow), it is recommended to pass the access token here to set the `at_hash` claim //! // automatically. //! Some(&access_token), //! // When returning the ID token alongside an authorization code (e.g., in the implicit //! // flow), it is recommended to pass the authorization code here to set the `c_hash` claim //! // automatically. //! None, //! )?; //! //! Ok(CoreTokenResponse::new( //! AccessToken::new("some_secret".to_string()), //! CoreTokenType::Bearer, //! CoreIdTokenFields::new(Some(id_token), EmptyExtraTokenFields {}), //! )) //! # } //! ``` //! //! # Asynchronous API //! //! An asynchronous API for async/await is also provided. //! //! ## Example //! //! ```rust,no_run //! use anyhow::anyhow; //! use openidconnect::{ //! AccessTokenHash, //! AuthenticationFlow, //! AuthorizationCode, //! ClientId, //! ClientSecret, //! CsrfToken, //! IssuerUrl, //! Nonce, //! OAuth2TokenResponse, //! PkceCodeChallenge, //! RedirectUrl, //! Scope, //! TokenResponse, //! }; //! use openidconnect::core::{ //! CoreAuthenticationFlow, //! CoreClient, //! CoreProviderMetadata, //! CoreResponseType, //! }; //! # #[cfg(feature = "reqwest")] //! use openidconnect::reqwest; //! use url::Url; //! //! //! # #[cfg(feature = "reqwest")] //! # async fn err_wrapper() -> Result<(), anyhow::Error> { //! let http_client = reqwest::ClientBuilder::new() //! // Following redirects opens the client up to SSRF vulnerabilities. //! .redirect(reqwest::redirect::Policy::none()) //! .build() //! .expect("Client should build"); //! //! // Use OpenID Connect Discovery to fetch the provider metadata. //! let provider_metadata = CoreProviderMetadata::discover_async( //! IssuerUrl::new("https://accounts.example.com".to_string())?, //! &http_client, //! ) //! .await?; //! //! // Create an OpenID Connect client by specifying the client ID, client secret, authorization URL //! // and token URL. //! let client = //! CoreClient::from_provider_metadata( //! provider_metadata, //! ClientId::new("client_id".to_string()), //! Some(ClientSecret::new("client_secret".to_string())), //! ) //! // Set the URL the user will be redirected to after the authorization process. //! .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?); //! //! // Generate a PKCE challenge. //! let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); //! //! // Generate the full authorization URL. //! let (auth_url, csrf_token, nonce) = client //! .authorize_url( //! CoreAuthenticationFlow::AuthorizationCode, //! CsrfToken::new_random, //! Nonce::new_random, //! ) //! // Set the desired scopes. //! .add_scope(Scope::new("read".to_string())) //! .add_scope(Scope::new("write".to_string())) //! // Set the PKCE code challenge. //! .set_pkce_challenge(pkce_challenge) //! .url(); //! //! // This is the URL you should redirect the user to, in order to trigger the authorization //! // process. //! println!("Browse to: {}", auth_url); //! //! // Once the user has been redirected to the redirect URL, you'll have access to the //! // authorization code. For security reasons, your code should verify that the `state` //! // parameter returned by the server matches `csrf_state`. //! //! // Now you can exchange it for an access token and ID token. //! let token_response = //! client //! .exchange_code(AuthorizationCode::new("some authorization code".to_string()))? //! // Set the PKCE code verifier. //! .set_pkce_verifier(pkce_verifier) //! .request_async(&http_client) //! .await?; //! //! // Extract the ID token claims after verifying its authenticity and nonce. //! let id_token = token_response //! .id_token() //! .ok_or_else(|| anyhow!("Server did not return an ID token"))?; //! let id_token_verifier = client.id_token_verifier(); //! let claims = id_token.claims(&id_token_verifier, &nonce)?; //! //! // Verify the access token hash to ensure that the access token hasn't been substituted for //! // another user's. //! if let Some(expected_access_token_hash) = claims.access_token_hash() { //! let actual_access_token_hash = AccessTokenHash::from_token( //! token_response.access_token(), //! id_token.signing_alg()?, //! id_token.signing_key(&id_token_verifier)?, //! )?; //! if actual_access_token_hash != *expected_access_token_hash { //! return Err(anyhow!("Invalid access token")); //! } //! } //! //! // The authenticated user's identity is now available. See the IdTokenClaims struct for a //! // complete listing of the available claims. //! println!( //! "User {} with e-mail address {} has authenticated successfully", //! claims.subject().as_str(), //! claims.email().map(|email| email.as_str()).unwrap_or(""), //! ); //! //! // See the OAuth2TokenResponse trait for a listing of other available fields such as //! // access_token() and refresh_token(). //! //! # Ok(()) //! # } //! ``` use crate::jwt::{JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenHeader}; use crate::verification::{AudiencesClaim, IssuerClaim}; // Defined first since other modules need the macros, and definition order is significant for // macros. This module is private. #[macro_use] mod macros; /// Baseline OpenID Connect implementation and types. pub mod core; /// OpenID Connect Dynamic Client Registration. pub mod registration; // Private modules since we may move types between different modules; these are exported publicly // via the pub use above. mod authorization; mod claims; mod client; mod discovery; mod helpers; mod id_token; mod logout; mod token; mod types; mod user_info; mod verification; // Private module for HTTP(S) utilities. mod http_utils; // Private module for JWT utilities. mod jwt; pub use oauth2::{ AccessToken, AsyncHttpClient, AuthType, AuthUrl, AuthorizationCode, ClientCredentialsTokenRequest, ClientId, ClientSecret, CodeTokenRequest, ConfigurationError, CsrfToken, DeviceAccessTokenRequest, DeviceAuthorizationRequest, DeviceAuthorizationResponse, DeviceAuthorizationUrl, DeviceCode, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, EmptyExtraDeviceAuthorizationFields, EmptyExtraTokenFields, EndUserVerificationUrl, EndpointMaybeSet, EndpointNotSet, EndpointSet, EndpointState, ErrorResponse, ErrorResponseType, ExtraDeviceAuthorizationFields, ExtraTokenFields, HttpClientError, HttpRequest, HttpResponse, IntrospectionRequest, IntrospectionUrl, PasswordTokenRequest, PkceCodeChallenge, PkceCodeChallengeMethod, PkceCodeVerifier, RedirectUrl, RefreshToken, RefreshTokenRequest, RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, RevocableToken, RevocationErrorResponseType, RevocationRequest, RevocationUrl, Scope, StandardErrorResponse, StandardTokenIntrospectionResponse, StandardTokenResponse, SyncHttpClient, TokenIntrospectionResponse, TokenResponse as OAuth2TokenResponse, TokenType, TokenUrl, UserCode, VerificationUriComplete, }; /// Public re-exports of types used for HTTP client interfaces. pub use oauth2::http; pub use oauth2::url; #[cfg(all(feature = "curl", not(target_arch = "wasm32")))] pub use oauth2::curl; #[cfg(all(feature = "curl", not(target_arch = "wasm32")))] pub use oauth2::CurlHttpClient; #[cfg(all(feature = "curl", target_arch = "wasm32"))] compile_error!("wasm32 is not supported with the `curl` feature. Use the `reqwest` backend or a custom backend for wasm32 support"); #[cfg(any(feature = "reqwest", feature = "reqwest-blocking"))] pub use oauth2::reqwest; #[cfg(feature = "ureq")] pub use oauth2::ureq; pub use crate::authorization::{AuthenticationFlow, AuthorizationRequest}; pub use crate::claims::{ AdditionalClaims, AddressClaim, EmptyAdditionalClaims, GenderClaim, StandardClaims, }; pub use crate::client::Client; pub use crate::discovery::{ AdditionalProviderMetadata, DiscoveryError, EmptyAdditionalProviderMetadata, ProviderMetadata, }; pub use crate::id_token::IdTokenFields; pub use crate::id_token::{IdToken, IdTokenClaims}; pub use crate::jwt::{JsonWebTokenError, JsonWebTokenType, NormalizedJsonWebTokenType}; pub use crate::logout::{LogoutProviderMetadata, LogoutRequest, ProviderMetadataWithLogout}; pub use crate::token::TokenResponse; // Flatten the module hierarchy involving types. They're only separated to improve code // organization. pub use crate::types::jwk::{ JsonWebKey, JsonWebKeyAlgorithm, JsonWebKeyId, JsonWebKeyType, JsonWebKeyUse, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, PrivateSigningKey, }; pub use crate::types::jwks::{JsonWebKeySet, JsonWebKeySetUrl}; pub use crate::types::localized::{LanguageTag, LocalizedClaim}; pub use crate::types::{ AccessTokenHash, AddressCountry, AddressLocality, AddressPostalCode, AddressRegion, ApplicationType, Audience, AuthDisplay, AuthPrompt, AuthenticationContextClass, AuthenticationMethodReference, AuthorizationCodeHash, ClaimName, ClaimType, ClientAuthMethod, ClientConfigUrl, ClientContactEmail, ClientName, ClientUrl, EndSessionUrl, EndUserBirthday, EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, EndUserWebsiteUrl, FormattedAddress, GrantType, InitiateLoginUrl, IssuerUrl, LoginHint, LogoUrl, LogoutHint, Nonce, OpPolicyUrl, OpTosUrl, PolicyUrl, PostLogoutRedirectUrl, RegistrationAccessToken, RegistrationUrl, RequestUrl, ResponseMode, ResponseType, ResponseTypes, SectorIdentifierUrl, ServiceDocUrl, SigningError, StreetAddress, SubjectIdentifier, SubjectIdentifierType, ToSUrl, }; pub use crate::user_info::{ UserInfoClaims, UserInfoError, UserInfoJsonWebToken, UserInfoRequest, UserInfoResponseType, UserInfoUrl, }; pub use crate::verification::{ ClaimsVerificationError, IdTokenVerifier, NonceVerifier, SignatureVerificationError, UserInfoVerifier, }; ================================================ FILE: src/logout.rs ================================================ use crate::core::{ CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, }; use crate::helpers::join_vec; use crate::types::{LogoutHint, PostLogoutRedirectUrl}; use crate::{ AdditionalClaims, AdditionalProviderMetadata, ClientId, CsrfToken, EmptyAdditionalProviderMetadata, EndSessionUrl, GenderClaim, IdToken, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, ProviderMetadata, }; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use url::Url; /// Additional metadata for providers implementing [OpenID Connect RP-Initiated /// Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html). #[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LogoutProviderMetadata where A: AdditionalProviderMetadata, { /// The end session endpoint as described in [OpenID Connect RP-Initiated /// Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html). pub end_session_endpoint: Option, #[serde(bound = "A: AdditionalProviderMetadata", flatten)] /// A field for an additional struct implementing AdditionalProviderMetadata. pub additional_metadata: A, } impl AdditionalProviderMetadata for LogoutProviderMetadata where A: AdditionalProviderMetadata {} /// Provider metadata returned by [OpenID Connect Discovery]( /// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata) /// that returns [`ProviderMetadata::additional_metadata`] for providers /// implementing [OpenID Connect RP-Initiated Logout 1.0]( /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html). pub type ProviderMetadataWithLogout = ProviderMetadata< LogoutProviderMetadata, CoreAuthDisplay, CoreClientAuthMethod, CoreClaimName, CoreClaimType, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, >; /// A request to the end session endpoint. pub struct LogoutRequest { end_session_endpoint: EndSessionUrl, parameters: LogoutRequestParameters, } #[derive(Default)] struct LogoutRequestParameters { id_token_hint: Option, logout_hint: Option, client_id: Option, post_logout_redirect_uri: Option, state: Option, ui_locales: Vec, } impl From for LogoutRequest { fn from(value: EndSessionUrl) -> Self { LogoutRequest { end_session_endpoint: value, parameters: Default::default(), } } } impl LogoutRequest { /// Provides an ID token previously issued by this OpenID Connect Provider as a hint about /// the user's identity. pub fn set_id_token_hint( mut self, id_token_hint: &IdToken, ) -> Self where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { self.parameters.id_token_hint = Some(id_token_hint.to_string()); self } /// Provides the OpenID Connect Provider with a hint about the user's identity. /// /// The nature of this hint is specific to each provider. pub fn set_logout_hint(mut self, logout_hint: LogoutHint) -> Self { self.parameters.logout_hint = Some(logout_hint); self } /// Provides the OpenID Connect Provider with the client identifier. /// /// When both this and `id_token_hint` are set, the provider must verify that /// this client id matches the one used when the ID token was issued. pub fn set_client_id(mut self, client_id: ClientId) -> Self { self.parameters.client_id = Some(client_id); self } /// Provides the OpenID Connect Provider with a URI to redirect to after /// the logout has been performed. pub fn set_post_logout_redirect_uri(mut self, redirect_uri: PostLogoutRedirectUrl) -> Self { self.parameters.post_logout_redirect_uri = Some(redirect_uri); self } /// Specify an opaque value that the OpenID Connect Provider should pass back /// to your application using the state parameter when redirecting to post_logout_redirect_uri. pub fn set_state(mut self, state: CsrfToken) -> Self { self.parameters.state = Some(state); self } /// Requests the preferred languages for the user interface presented by the OpenID Connect /// Provider. /// /// Languages should be added in order of preference. pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self { self.parameters.ui_locales.push(ui_locale); self } /// Returns the full logout URL. In order to logout, a GET request should be made to this URL /// by the client's browser. pub fn http_get_url(self) -> Url { let mut url = self.end_session_endpoint.url().to_owned(); { let mut query = url.query_pairs_mut(); macro_rules! add_pair { ($name:ident, $acc:expr) => { if let Some($name) = self.parameters.$name { query.append_pair(stringify!($name), $acc); } }; } add_pair!(id_token_hint, id_token_hint.as_str()); add_pair!(logout_hint, logout_hint.secret()); add_pair!(client_id, client_id.as_str()); add_pair!(post_logout_redirect_uri, post_logout_redirect_uri.as_str()); add_pair!(state, state.secret()); if !self.parameters.ui_locales.is_empty() { query.append_pair("ui_locales", &join_vec(&self.parameters.ui_locales)); } } if url.query() == Some("") { url.set_query(None); } url } } #[cfg(test)] mod tests { use crate::core::{ CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, }; use crate::types::{LogoutHint, PostLogoutRedirectUrl}; use crate::{ AuthUrl, ClientId, CsrfToken, EmptyAdditionalClaims, EndSessionUrl, IdToken, IssuerUrl, JsonWebKeySetUrl, LanguageTag, LogoutProviderMetadata, LogoutRequest, ProviderMetadataWithLogout, }; use url::Url; use std::str::FromStr; #[test] fn test_end_session_endpoint_deserialization() { // Fetched from: https://rp.certification.openid.net:8080/openidconnect-rs/ // rp-response_type-code/.well-known/openid-configuration // But pared down let json_response = "{\ \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ \"response_types_supported\":[],\ \"subject_types_supported\":[],\ \"id_token_signing_alg_values_supported\": [],\ \"end_session_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session\",\ \"version\":\"3.0\"}"; let new_provider_metadata = ProviderMetadataWithLogout::new( IssuerUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" .to_string(), ) .unwrap(), AuthUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/\ rp-response_type-code/authorization" .to_string(), ) .unwrap(), JsonWebKeySetUrl::new( "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" .to_string(), ) .unwrap(), vec![], vec![], vec![], LogoutProviderMetadata { end_session_endpoint: Some(EndSessionUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" .to_string() ).unwrap()), additional_metadata: Default::default(), }, ); let provider_metadata: ProviderMetadataWithLogout = serde_json::from_str(json_response).unwrap(); assert_eq!(provider_metadata, new_provider_metadata); assert_eq!( Some(EndSessionUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" .to_string() ).unwrap()), provider_metadata.additional_metadata().end_session_endpoint ); } #[test] fn test_logout_request_with_no_parameters() { let endpoint = EndSessionUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" .to_string() ).unwrap(); let logout_url = LogoutRequest::from(endpoint).http_get_url(); assert_eq!( Url::parse( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" ).unwrap(), logout_url ); } #[test] fn test_logout_request_with_all_parameters() { let endpoint = EndSessionUrl::new( "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" .to_string() ).unwrap(); let logout_url = LogoutRequest::from(endpoint) .set_id_token_hint( &IdToken::< EmptyAdditionalClaims, CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, >::from_str( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwcz\ ovL3JwLmNlcnRpZmljYXRpb24ub3BlbmlkLm5ldDo4MDgwLyIsImV4c\ CI6MTUxNjIzOTAyMiwiaWF0IjoxNTE2MjM5MDIyLCJzdWIiOiJhc2Rm\ In0.cPwX6csO2uBEOZLVAGR7x5rHLRfD36MHpPy3JTk6orM", ) .unwrap(), ) .set_logout_hint(LogoutHint::new("johndoe".to_string())) .set_client_id(ClientId::new("asdf".to_string())) .set_post_logout_redirect_uri( PostLogoutRedirectUrl::new("https://localhost:8000/".to_string()).unwrap(), ) .set_state(CsrfToken::new("asdf".to_string())) .add_ui_locale(LanguageTag::new("en-US".to_string())) .add_ui_locale(LanguageTag::new("fr-FR".to_string())) .http_get_url(); assert_eq!( Url::parse( "https://rp.certification.openid.net:8080/openidconnect-rs\ /rp-response_type-code/end_session?id_token_hint=eyJhbGciO\ iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JwLmNlcn\ RpZmljYXRpb24ub3BlbmlkLm5ldDo4MDgwLyIsImV4cCI6MTUxNjIzOTAy\ MiwiaWF0IjoxNTE2MjM5MDIyLCJzdWIiOiJhc2RmIn0.cPwX6csO2uBEOZ\ LVAGR7x5rHLRfD36MHpPy3JTk6orM&logout_hint=johndoe&client_i\ d=asdf&post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A\ 8000%2F&state=asdf&ui_locales=en-US+fr-FR" ) .unwrap(), logout_url ); } } ================================================ FILE: src/macros.rs ================================================ /// Copied from oauth2-rs crate (not part of that crate's stable public interface). macro_rules! new_type { // Convenience pattern without an impl. ( $(#[$attr:meta])* $name:ident( $(#[$type_attr:meta])* $type:ty ) ) => { new_type![ @new_type_pub $(#[$attr])*, $name( $(#[$type_attr])* $type ), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), impl {} ]; }; // Convenience pattern without an impl. ( $(#[$attr:meta])* pub(crate) $name:ident( $(#[$type_attr:meta])* $type:ty ) ) => { new_type![ @new_type_pub_crate $(#[$attr])*, $name( $(#[$type_attr])* $type ), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), impl {} ]; }; // Main entry point with an impl. ( $(#[$attr:meta])* $name:ident( $(#[$type_attr:meta])* $type:ty ) impl { $($item:tt)* } ) => { new_type![ @new_type_pub $(#[$attr])*, $name( $(#[$type_attr])* $type ), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), impl { $($item)* } ]; }; // Main entry point with an impl. ( $(#[$attr:meta])* pub(crate) $name:ident( $(#[$type_attr:meta])* $type:ty ) impl { $($item:tt)* } ) => { new_type![ @new_type_pub_crate $(#[$attr])*, $name( $(#[$type_attr])* $type ), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), impl { $($item)* } ]; }; // Actual implementation, after stringifying the #[doc] attr. ( @new_type_pub $(#[$attr:meta])*, $name:ident( $(#[$type_attr:meta])* $type:ty ), $new_doc:expr, impl { $($item:tt)* } ) => { $(#[$attr])* #[derive(Clone, Debug, PartialEq, Eq)] pub struct $name( $(#[$type_attr])* $type ); impl $name { $($item)* #[allow(dead_code)] #[doc = $new_doc] pub fn new(s: $type) -> Self { $name(s) } } impl std::ops::Deref for $name { type Target = $type; fn deref(&self) -> &$type { &self.0 } } impl From<$name> for $type { fn from(t: $name) -> $type { t.0 } } }; // Actual implementation, after stringifying the #[doc] attr. ( @new_type_pub_crate $(#[$attr:meta])*, $name:ident( $(#[$type_attr:meta])* $type:ty ), $new_doc:expr, impl { $($item:tt)* } ) => { $(#[$attr])* #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct $name( $(#[$type_attr])* $type ); impl $name { $($item)* #[doc = $new_doc] pub const fn new(s: $type) -> Self { $name(s) } } impl std::ops::Deref for $name { type Target = $type; fn deref(&self) -> &$type { &self.0 } } impl From<$name> for $type { fn from(t: $name) -> $type { t.0 } } }; } /// Copied from oauth2-rs crate (not part of that crate's stable public interface). macro_rules! new_secret_type { ( $(#[$attr:meta])* $name:ident($type:ty) ) => { new_secret_type![ $(#[$attr])* $name($type) impl {} ]; }; ( $(#[$attr:meta])* $name:ident($type:ty) impl { $($item:tt)* } ) => { new_secret_type![ $(#[$attr])*, $name($type), concat!( "Create a new `", stringify!($name), "` to wrap the given `", stringify!($type), "`." ), concat!("Get the secret contained within this `", stringify!($name), "`."), impl { $($item)* } ]; }; ( $(#[$attr:meta])*, $name:ident($type:ty), $new_doc:expr, $secret_doc:expr, impl { $($item:tt)* } ) => { $( #[$attr] )* #[cfg_attr(feature = "timing-resistant-secret-traits", derive(Eq))] pub struct $name($type); impl $name { $($item)* #[doc = $new_doc] pub fn new(s: $type) -> Self { $name(s) } #[doc = $secret_doc] /// /// # Security Warning /// /// Leaking this value may compromise the security of the OAuth2 flow. pub fn secret(&self) -> &$type { &self.0 } #[doc = $secret_doc] /// /// # Security Warning /// /// Leaking this value may compromise the security of the OAuth2 flow. pub fn into_secret(self) -> $type { self.0 } } impl Debug for $name { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, concat!(stringify!($name), "([redacted])")) } } #[cfg(any(test, feature = "timing-resistant-secret-traits"))] impl PartialEq for $name { fn eq(&self, other: &Self) -> bool { ::digest(&self.0) == ::digest(&other.0) } } #[cfg(feature = "timing-resistant-secret-traits")] impl std::hash::Hash for $name { fn hash(&self, state: &mut H) { ::digest(&self.0).hash(state) } } }; } /// Creates a URL-specific new type /// /// Types created by this macro enforce during construction that the contained value represents a /// syntactically valid URL. However, comparisons and hashes of these types are based on the string /// representation given during construction, disregarding any canonicalization performed by the /// underlying `Url` struct. OpenID Connect requires certain URLs (e.g., ID token issuers) to be /// compared exactly, without canonicalization. /// /// In addition to the raw string representation, these types include a `url` method to retrieve a /// parsed `Url` struct. macro_rules! new_url_type { // Convenience pattern without an impl. ( $(#[$attr:meta])* $name:ident ) => { new_url_type![ @new_type_pub $(#[$attr])*, $name, concat!("Create a new `", stringify!($name), "` from a `String` to wrap a URL."), concat!("Create a new `", stringify!($name), "` from a `Url` to wrap a URL."), concat!("Return this `", stringify!($name), "` as a parsed `Url`."), impl {} ]; }; // Main entry point with an impl. ( $(#[$attr:meta])* $name:ident impl { $($item:tt)* } ) => { new_url_type![ @new_type_pub $(#[$attr])*, $name, concat!("Create a new `", stringify!($name), "` from a `String` to wrap a URL."), concat!("Create a new `", stringify!($name), "` from a `Url` to wrap a URL."), concat!("Return this `", stringify!($name), "` as a parsed `Url`."), impl { $($item)* } ]; }; // Actual implementation, after stringifying the #[doc] attr. ( @new_type_pub $(#[$attr:meta])*, $name:ident, $new_doc:expr, $from_url_doc:expr, $url_doc:expr, impl { $($item:tt)* } ) => { $(#[$attr])* #[derive(Clone)] pub struct $name(url::Url, String); impl $name { #[doc = $new_doc] pub fn new(url: String) -> Result { Ok($name(url::Url::parse(&url)?, url)) } #[doc = $from_url_doc] pub fn from_url(url: url::Url) -> Self { let s = url.to_string(); Self(url, s) } #[doc = $url_doc] pub fn url(&self) -> &url::Url { return &self.0; } $($item)* } impl std::ops::Deref for $name { type Target = String; fn deref(&self) -> &String { &self.1 } } impl ::std::fmt::Display for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { write!(f, "{}", self.1) } } impl From<$name> for url::Url { fn from(t: $name) -> url::Url { t.0 } } impl ::std::fmt::Debug for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> { let mut debug_trait_builder = f.debug_tuple(stringify!($name)); debug_trait_builder.field(&self.1); debug_trait_builder.finish() } } impl<'de> ::serde::Deserialize<'de> for $name { fn deserialize(deserializer: D) -> Result where D: ::serde::de::Deserializer<'de>, { struct UrlVisitor; impl<'de> ::serde::de::Visitor<'de> for UrlVisitor { type Value = $name; fn expecting( &self, formatter: &mut ::std::fmt::Formatter ) -> ::std::fmt::Result { formatter.write_str(stringify!($name)) } fn visit_str(self, v: &str) -> Result where E: ::serde::de::Error, { $name::new(v.to_string()).map_err(E::custom) } } deserializer.deserialize_str(UrlVisitor {}) } } impl ::serde::Serialize for $name { fn serialize(&self, serializer: SE) -> Result where SE: ::serde::Serializer, { serializer.serialize_str(&self.1) } } impl ::std::hash::Hash for $name { fn hash(&self, state: &mut H) -> () { ::std::hash::Hash::hash(&(self.1), state); } } impl Ord for $name { fn cmp(&self, other: &$name) -> ::std::cmp::Ordering { self.1.cmp(&other.1) } } impl PartialOrd for $name { fn partial_cmp(&self, other: &$name) -> Option<::std::cmp::Ordering> { Some(self.cmp(other)) } } impl PartialEq for $name { fn eq(&self, other: &$name) -> bool { self.1 == other.1 } } impl Eq for $name {} }; } macro_rules! serialize_fields { (@case $self:ident $map:ident Option(Seconds($field:ident))) => { if let Some(ref $field) = $self.$field { $map.serialize_entry(stringify!($field), &$field.as_secs())?; } }; (@case $self:ident $map:ident Option(DateTime(Seconds($field:ident)))) => { if let Some(ref $field) = $self.$field { $map.serialize_entry(stringify!($field), &crate::helpers::Timestamp::from_utc(&$field))?; } }; (@case $self:ident $map:ident Option($field:ident)) => { if let Some(ref $field) = $self.$field { $map.serialize_entry(stringify!($field), $field)?; } }; (@case $self:ident $map:ident LanguageTag($field:ident)) => { if let Some(ref field_map) = $self.$field { use itertools::sorted; let sorted_field_map = sorted(field_map.iter()); for (language_tag_opt, $field) in sorted_field_map { if let Some(ref language_tag) = language_tag_opt { $map.serialize_entry( &format!(concat!(stringify!($field), "#{}"), language_tag.as_ref()), &$field )?; } else { $map.serialize_entry(stringify!($field), &$field)?; } } } }; (@case $self:ident $map:ident $field:ident) => { $map.serialize_entry(stringify!($field), &$self.$field)?; }; // Main entry point ( $self:ident -> $serializer:ident { $([$($entry:tt)+])+ } ) => { let mut map = $serializer.serialize_map(None)?; $( serialize_fields![@case $self map $($entry)+]; )+ map.end() }; } macro_rules! field_getters { (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident Option < bool >) => { #[doc = $doc] $vis fn $field(&$self) -> Option { $zero.$field } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident Option < bool > { $($body:tt)+ }) => { #[doc = $doc] $vis fn $field(&$self) -> Option { $($body)+ } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident Option < DateTime < Utc >>) => { #[doc = $doc] $vis fn $field(&$self) -> Option> { $zero.$field } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident Option < DateTime < Utc >> { $($body:tt)+ }) => { #[doc = $doc] $vis fn $field(&$self) -> Option> { $($body)+ } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident Option < $type:ty >) => { #[doc = $doc] $vis fn $field(&$self) -> Option<&$type> { $zero.$field.as_ref() } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident Option < $type:ty > { $($body:tt)+ }) => { #[doc = $doc] $vis fn $field(&$self) -> Option<$type> { $($body)+ } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident DateTime < Utc >) => { #[doc = $doc] $vis fn $field(&$self) -> DateTime { $zero.$field } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident $type:ty) => { #[doc = $doc] $vis fn $field(&$self) -> &$type { &$zero.$field } }; (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $field:ident $type:ty { $($body:tt)+ }) => { #[doc = $doc] $vis fn $field(&$self) -> $type { $($body)+ } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() Option < bool >) => { #[doc = $doc] fn $field(&$self) -> Option { $zero.$field() } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() Option < bool > { $($body:tt)+ }) => { #[doc = $doc] fn $field(&$self) -> Option { $($body)+ } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() Option < DateTime < Utc >>) => { #[doc = $doc] fn $field(&$self) -> Option> { $zero.$field() } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() Option < DateTime < Utc >> { $($body:tt)+ }) => { #[doc = $doc] fn $field(&$self) -> Option> { $($body)+ } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() Option < $type:ty >) => { #[doc = $doc] fn $field(&$self) -> Option<&$type> { $zero.$field() } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() Option < $type:ty > { $($body:tt)+ }) => { #[doc = $doc] fn $field(&$self) -> Option<$type> { $($body)+ } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() DateTime < Utc >) => { #[doc = $doc] fn $field(&$self) -> DateTime { $zero.$field() } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() $type:ty) => { #[doc = $doc] fn $field(&$self) -> &$type { &$zero.$field() } }; (@case [$doc:expr] $self:ident [$zero:expr] $field:ident() $type:ty { $($body:tt)+ }) => { #[doc = $doc] fn $field(&$self) -> $type { $($body)+ } }; // Main entry points ( $vis:vis $self:ident [$zero:expr] [$doc:expr] { $( $field:ident[$($entry:tt)+] [$doc_field:expr], )+ } ) => { $( field_getters![ @case [concat!("Returns the `", $doc_field, "` ", $doc, ".")] $vis $self [$zero] $field $($entry)+ ]; )+ }; ( $vis:vis $self:ident [$zero:expr]() [$doc:expr] { $( $field:ident[$($entry:tt)+] [$doc_field:expr], )+ } ) => { $( field_getters![ @case [concat!("Returns the `", $doc_field, "` ", $doc, ".")] $vis $self [$zero] $field() $($entry)+ ]; )+ }; } macro_rules! field_setters { (@case [$doc:expr] $vis:vis $self:ident [$zero:expr] $setter:ident $field:ident $type:ty [$doc_field:expr]) => { field_setters![ @case2 [concat!("Sets the `", $doc_field, "` ", $doc, ".")] $vis $self [$zero] $setter $field $type ]; }; (@case2 [$doc:expr] $vis:vis $self:ident [$zero:expr] $setter:ident $field:ident $type:ty) => { #[doc = $doc] $vis fn $setter( mut $self, $field: $type ) -> Self { $zero.$field = $field; $self } }; // Main entry point ( $vis:vis $self:ident [$zero:expr] [$doc:expr] { $setter:ident -> $field:ident[$($entry:tt)+] [$doc_field:expr] } ) => { field_setters![ @case [$doc] $vis $self [$zero] $setter $field $($entry)+ [$doc_field] ]; }; } macro_rules! field_getters_setters { ( @single $vis:vis $self:ident [$zero:expr] [$doc:expr] [$setter:ident -> $field:ident[$($entry:tt)+] [$field_doc:expr], $($rest:tt)*] ) => { field_getters![$vis $self [$zero] [$doc] { $field[$($entry)+] [$field_doc], }]; field_setters![ $vis $self [$zero] [$doc] { $setter -> $field[$($entry)+] [$field_doc] } ]; field_getters_setters![@single $vis $self [$zero] [$doc] [$($rest)*]]; }; ( @single $vis:vis $self:ident [$zero:expr]() [$doc:expr] [$setter:ident -> $field:ident[$($entry:tt)+] [$field_doc:expr], $($rest:tt)*] ) => { field_getters![$vis $self [$zero]() [$doc] { $field[$($entry)+] [$field_doc], }]; field_setters![ $vis $self [$zero] [$doc] { $setter -> $field[$($entry)+] [$field_doc] } ]; field_getters_setters![@single $vis $self [$zero]() [$doc] [$($rest)*]]; }; ( @single $vis:vis $self:ident [$zero:expr] [$doc:expr] [$setter:ident -> $field:ident[$($entry:tt)+], $($rest:tt)*] ) => { field_getters![$vis $self [$zero] [$doc] { $field[$($entry)+] [stringify!($field)], }]; field_setters![ $vis $self [$zero] [$doc] { $setter -> $field[$($entry)+] [stringify!($field)] } ]; field_getters_setters![@single $vis $self [$zero] [$doc] [$($rest)*]]; }; ( @single $vis:vis $self:ident [$zero:expr]() [$doc:expr] [$setter:ident -> $field:ident[$($entry:tt)+], $($rest:tt)*] ) => { field_getters![$vis $self [$zero]() [$doc] { $field[$($entry)+] [stringify!($field)], }]; field_setters![ $vis $self [$zero] [$doc] { $setter -> $field[$($entry)+] [stringify!($field)] } ]; field_getters_setters![@single $vis $self [$zero]() [$doc] [$($rest)*]]; }; // Base case. (@single $vis:vis $self:ident [$zero:expr] [$doc:expr] []) => {}; // Main entry points. ( $vis:vis $self:ident [$zero:expr] [$doc:expr] { $setter:ident -> $field:ident[$($entry:tt)+] $($rest:tt)* } ) => { field_getters_setters![ @single $vis $self [$zero] [$doc] [$setter -> $field[$($entry)+] $($rest)*] ]; }; ( $vis:vis $self:ident [$zero:expr]() [$doc:expr] { $setter:ident -> $field:ident[$($entry:tt)+] $($rest:tt)* } ) => { field_getters_setters![ @single $vis $self [$zero]() [$doc] [$setter -> $field[$($entry)+] $($rest)*] ]; }; } macro_rules! deserialize_from_str { ($type:path) => { impl<'de> serde::Deserialize<'de> for $type { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { let variant_str = String::deserialize(deserializer)?; Ok(Self::from_str(&variant_str)) } } }; } macro_rules! serialize_as_str { ($type:path) => { impl serde::ser::Serialize for $type { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(self.as_ref()) } } }; } ================================================ FILE: src/registration/mod.rs ================================================ use crate::helpers::{DeserializeMapField, Timestamp}; use crate::http_utils::{auth_bearer, check_content_type, MIME_TYPE_JSON}; use crate::types::localized::split_language_tag_key; use crate::types::{ ApplicationType, AuthenticationContextClass, ClientAuthMethod, ClientConfigUrl, ClientContactEmail, ClientName, ClientUrl, GrantType, InitiateLoginUrl, LogoUrl, PolicyUrl, RegistrationAccessToken, RegistrationUrl, RequestUrl, ResponseType, ResponseTypes, SectorIdentifierUrl, SubjectIdentifierType, ToSUrl, }; use crate::{ AccessToken, AsyncHttpClient, ClientId, ClientSecret, ErrorResponseType, HttpRequest, HttpResponse, JsonWebKey, JsonWebKeySet, JsonWebKeySetUrl, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, LocalizedClaim, RedirectUrl, StandardErrorResponse, SyncHttpClient, }; use chrono::{DateTime, Utc}; use http::header::{HeaderValue, ACCEPT, CONTENT_TYPE}; use http::method::Method; use http::status::StatusCode; use serde::de::{DeserializeOwned, Deserializer, MapAccess, Visitor}; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize, Serializer}; use serde_with::{serde_as, skip_serializing_none}; use thiserror::Error; use std::fmt::{Debug, Formatter, Result as FormatterResult}; use std::future::Future; use std::marker::PhantomData; use std::time::Duration; #[cfg(test)] mod tests; /// Trait for adding extra fields to [`ClientMetadata`]. pub trait AdditionalClientMetadata: Debug + DeserializeOwned + Serialize {} // In order to support serde flatten, this must be an empty struct rather than an empty // tuple struct. /// Empty (default) extra [`ClientMetadata`] fields. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct EmptyAdditionalClientMetadata {} impl AdditionalClientMetadata for EmptyAdditionalClientMetadata {} /// Client metadata used in dynamic client registration. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ClientMetadata where A: AdditionalClientMetadata, AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { // To avoid implementing a custom deserializer that handles both language tags and flatten, // we wrap the language tag handling in its own flattened struct. #[serde(bound = "AT: ApplicationType", flatten)] standard_metadata: StandardClientMetadata, #[serde(bound = "A: AdditionalClientMetadata", flatten)] additional_metadata: A, } impl ClientMetadata where A: AdditionalClientMetadata, AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { /// Instantiates new client metadata. pub fn new(redirect_uris: Vec, additional_metadata: A) -> Self { Self { standard_metadata: StandardClientMetadata { redirect_uris, response_types: None, grant_types: None, application_type: None, contacts: None, client_name: None, logo_uri: None, client_uri: None, policy_uri: None, tos_uri: None, jwks_uri: None, jwks: None, sector_identifier_uri: None, subject_type: None, id_token_signed_response_alg: None, id_token_encrypted_response_alg: None, id_token_encrypted_response_enc: None, userinfo_signed_response_alg: None, userinfo_encrypted_response_alg: None, userinfo_encrypted_response_enc: None, request_object_signing_alg: None, request_object_encryption_alg: None, request_object_encryption_enc: None, token_endpoint_auth_method: None, token_endpoint_auth_signing_alg: None, default_max_age: None, require_auth_time: None, default_acr_values: None, initiate_login_uri: None, request_uris: None, }, additional_metadata, } } field_getters_setters![ pub self [self.standard_metadata] ["client metadata value"] { set_redirect_uris -> redirect_uris[Vec], set_response_types -> response_types[Option>>], set_grant_types -> grant_types[Option>], set_application_type -> application_type[Option], set_contacts -> contacts[Option>], set_client_name -> client_name[Option>], set_logo_uri -> logo_uri[Option>], set_client_uri -> client_uri[Option>], set_policy_uri -> policy_uri[Option>], set_tos_uri -> tos_uri[Option>], set_jwks_uri -> jwks_uri[Option], set_jwks -> jwks[Option>], set_sector_identifier_uri -> sector_identifier_uri[Option], set_subject_type -> subject_type[Option], set_id_token_signed_response_alg -> id_token_signed_response_alg[Option], set_id_token_encrypted_response_alg -> id_token_encrypted_response_alg[Option], set_id_token_encrypted_response_enc -> id_token_encrypted_response_enc[Option], set_userinfo_signed_response_alg -> userinfo_signed_response_alg[Option], set_userinfo_encrypted_response_alg -> userinfo_encrypted_response_alg[Option], set_userinfo_encrypted_response_enc -> userinfo_encrypted_response_enc[Option], set_request_object_signing_alg -> request_object_signing_alg[Option], set_request_object_encryption_alg -> request_object_encryption_alg[Option], set_request_object_encryption_enc -> request_object_encryption_enc[Option], set_token_endpoint_auth_method -> token_endpoint_auth_method[Option], set_token_endpoint_auth_signing_alg -> token_endpoint_auth_signing_alg[Option], set_default_max_age -> default_max_age[Option], set_require_auth_time -> require_auth_time[Option], set_default_acr_values -> default_acr_values[Option>], set_initiate_login_uri -> initiate_login_uri[Option], set_request_uris -> request_uris[Option>], } ]; /// Returns additional client metadata fields. pub fn additional_metadata(&self) -> &A { &self.additional_metadata } /// Returns mutable additional client metadata fields. pub fn additional_metadata_mut(&mut self) -> &mut A { &mut self.additional_metadata } } #[derive(Clone, Debug, PartialEq)] struct StandardClientMetadata where AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { redirect_uris: Vec, response_types: Option>>, grant_types: Option>, application_type: Option, contacts: Option>, client_name: Option>, logo_uri: Option>, client_uri: Option>, policy_uri: Option>, tos_uri: Option>, jwks_uri: Option, jwks: Option>, sector_identifier_uri: Option, subject_type: Option, id_token_signed_response_alg: Option, id_token_encrypted_response_alg: Option, id_token_encrypted_response_enc: Option, userinfo_signed_response_alg: Option, userinfo_encrypted_response_alg: Option, userinfo_encrypted_response_enc: Option, request_object_signing_alg: Option, request_object_encryption_alg: Option, request_object_encryption_enc: Option, token_endpoint_auth_method: Option, token_endpoint_auth_signing_alg: Option, default_max_age: Option, require_auth_time: Option, default_acr_values: Option>, initiate_login_uri: Option, request_uris: Option>, } impl<'de, AT, CA, G, JE, JK, K, RT, S> Deserialize<'de> for StandardClientMetadata where AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { /// Special deserializer that supports [RFC 5646](https://tools.ietf.org/html/rfc5646) language /// tags associated with human-readable client metadata fields. fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct MetadataVisitor< AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, >(PhantomData<(AT, CA, G, JE, JK, K, RT, S)>); impl<'de, AT, CA, G, JE, JK, K, RT, S> Visitor<'de> for MetadataVisitor where AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { type Value = StandardClientMetadata; fn expecting(&self, formatter: &mut Formatter) -> FormatterResult { formatter.write_str("struct StandardClientMetadata") } fn visit_map(self, mut map: V) -> Result where V: MapAccess<'de>, { // NB: The non-localized fields are actually Option> here so that we can // distinguish between omitted fields and fields explicitly set to `null`. The // latter is necessary so that we can detect duplicate fields (e.g., if a key is // present both with a null value and a non-null value, that's an error). let mut redirect_uris = None; let mut response_types = None; let mut grant_types = None; let mut application_type = None; let mut contacts = None; let mut client_name = None; let mut logo_uri = None; let mut client_uri = None; let mut policy_uri = None; let mut tos_uri = None; let mut jwks_uri = None; let mut jwks = None; let mut sector_identifier_uri = None; let mut subject_type = None; let mut id_token_signed_response_alg = None; let mut id_token_encrypted_response_alg = None; let mut id_token_encrypted_response_enc = None; let mut userinfo_signed_response_alg = None; let mut userinfo_encrypted_response_alg = None; let mut userinfo_encrypted_response_enc = None; let mut request_object_signing_alg = None; let mut request_object_encryption_alg = None; let mut request_object_encryption_enc = None; let mut token_endpoint_auth_method = None; let mut token_endpoint_auth_signing_alg = None; let mut default_max_age = None; let mut require_auth_time = None; let mut default_acr_values = None; let mut initiate_login_uri = None; let mut request_uris = None; macro_rules! field_case { ($field:ident, $typ:ty, $language_tag:ident) => {{ $field = Some(<$typ>::deserialize_map_field( &mut map, stringify!($field), $language_tag, $field, )?); }}; } while let Some(key) = map.next_key::()? { let (field_name, language_tag) = split_language_tag_key(&key); match field_name { "redirect_uris" => field_case!(redirect_uris, Vec<_>, language_tag), "response_types" => field_case!(response_types, Option<_>, language_tag), "grant_types" => field_case!(grant_types, Option<_>, language_tag), "application_type" => { field_case!(application_type, Option<_>, language_tag) } "contacts" => field_case!(contacts, Option<_>, language_tag), "client_name" => { field_case!(client_name, LocalizedClaim>, language_tag) } "logo_uri" => { field_case!(logo_uri, LocalizedClaim>, language_tag) } "client_uri" => { field_case!(client_uri, LocalizedClaim>, language_tag) } "policy_uri" => { field_case!(policy_uri, LocalizedClaim>, language_tag) } "tos_uri" => field_case!(tos_uri, LocalizedClaim>, language_tag), "jwks_uri" => field_case!(jwks_uri, Option<_>, language_tag), "jwks" => field_case!(jwks, Option<_>, language_tag), "sector_identifier_uri" => { field_case!(sector_identifier_uri, Option<_>, language_tag) } "subject_type" => field_case!(subject_type, Option<_>, language_tag), "id_token_signed_response_alg" => { field_case!(id_token_signed_response_alg, Option<_>, language_tag) } "id_token_encrypted_response_alg" => { field_case!(id_token_encrypted_response_alg, Option<_>, language_tag) } "id_token_encrypted_response_enc" => { field_case!(id_token_encrypted_response_enc, Option<_>, language_tag) } "userinfo_signed_response_alg" => { field_case!(userinfo_signed_response_alg, Option<_>, language_tag) } "userinfo_encrypted_response_alg" => { field_case!(userinfo_encrypted_response_alg, Option<_>, language_tag) } "userinfo_encrypted_response_enc" => { field_case!(userinfo_encrypted_response_enc, Option<_>, language_tag) } "request_object_signing_alg" => { field_case!(request_object_signing_alg, Option<_>, language_tag) } "request_object_encryption_alg" => { field_case!(request_object_encryption_alg, Option<_>, language_tag) } "request_object_encryption_enc" => { field_case!(request_object_encryption_enc, Option<_>, language_tag) } "token_endpoint_auth_method" => { field_case!(token_endpoint_auth_method, Option<_>, language_tag) } "token_endpoint_auth_signing_alg" => { field_case!(token_endpoint_auth_signing_alg, Option<_>, language_tag) } "default_max_age" => { field_case!(default_max_age, Option, language_tag) } "require_auth_time" => { field_case!(require_auth_time, Option<_>, language_tag) } "default_acr_values" => { field_case!(default_acr_values, Option<_>, language_tag) } "initiate_login_uri" => { field_case!(initiate_login_uri, Option<_>, language_tag) } "request_uris" => field_case!(request_uris, Option<_>, language_tag), // Ignore unknown fields. _ => { map.next_value::()?; continue; } }; } Ok(StandardClientMetadata { redirect_uris: redirect_uris .ok_or_else(|| serde::de::Error::missing_field("redirect_uris"))?, response_types: response_types.flatten(), grant_types: grant_types.flatten(), application_type: application_type.flatten(), contacts: contacts.flatten(), client_name: client_name.and_then(LocalizedClaim::flatten_or_none), logo_uri: logo_uri.and_then(LocalizedClaim::flatten_or_none), client_uri: client_uri.and_then(LocalizedClaim::flatten_or_none), policy_uri: policy_uri.and_then(LocalizedClaim::flatten_or_none), tos_uri: tos_uri.and_then(LocalizedClaim::flatten_or_none), jwks_uri: jwks_uri.flatten(), jwks: jwks.flatten(), sector_identifier_uri: sector_identifier_uri.flatten(), subject_type: subject_type.flatten(), id_token_signed_response_alg: id_token_signed_response_alg.flatten(), id_token_encrypted_response_alg: id_token_encrypted_response_alg.flatten(), id_token_encrypted_response_enc: id_token_encrypted_response_enc.flatten(), userinfo_signed_response_alg: userinfo_signed_response_alg.flatten(), userinfo_encrypted_response_alg: userinfo_encrypted_response_alg.flatten(), userinfo_encrypted_response_enc: userinfo_encrypted_response_enc.flatten(), request_object_signing_alg: request_object_signing_alg.flatten(), request_object_encryption_alg: request_object_encryption_alg.flatten(), request_object_encryption_enc: request_object_encryption_enc.flatten(), token_endpoint_auth_method: token_endpoint_auth_method.flatten(), token_endpoint_auth_signing_alg: token_endpoint_auth_signing_alg.flatten(), default_max_age: default_max_age.flatten().map(Duration::from_secs), require_auth_time: require_auth_time.flatten(), default_acr_values: default_acr_values.flatten(), initiate_login_uri: initiate_login_uri.flatten(), request_uris: request_uris.flatten(), }) } } deserializer.deserialize_map(MetadataVisitor(PhantomData)) } } impl Serialize for StandardClientMetadata where AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { #[allow(clippy::cognitive_complexity)] fn serialize(&self, serializer: SE) -> Result where SE: Serializer, { serialize_fields! { self -> serializer { [redirect_uris] [Option(response_types)] [Option(grant_types)] [Option(application_type)] [Option(contacts)] [LanguageTag(client_name)] [LanguageTag(logo_uri)] [LanguageTag(client_uri)] [LanguageTag(policy_uri)] [LanguageTag(tos_uri)] [Option(jwks_uri)] [Option(jwks)] [Option(sector_identifier_uri)] [Option(subject_type)] [Option(id_token_signed_response_alg)] [Option(id_token_encrypted_response_alg)] [Option(id_token_encrypted_response_enc)] [Option(userinfo_signed_response_alg)] [Option(userinfo_encrypted_response_alg)] [Option(userinfo_encrypted_response_enc)] [Option(request_object_signing_alg)] [Option(request_object_encryption_alg)] [Option(request_object_encryption_enc)] [Option(token_endpoint_auth_method)] [Option(token_endpoint_auth_signing_alg)] [Option(Seconds(default_max_age))] [Option(require_auth_time)] [Option(default_acr_values)] [Option(initiate_login_uri)] [Option(request_uris)] } } } } /// Dynamic client registration request. #[derive(Clone, Debug)] pub struct ClientRegistrationRequest where AC: AdditionalClientMetadata, AR: AdditionalClientRegistrationResponse, AT: ApplicationType, CA: ClientAuthMethod, ET: RegisterErrorResponseType, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { client_metadata: ClientMetadata, initial_access_token: Option, _phantom: PhantomData<(AR, ET)>, } impl ClientRegistrationRequest where AC: AdditionalClientMetadata, AR: AdditionalClientRegistrationResponse, AT: ApplicationType, CA: ClientAuthMethod, ET: RegisterErrorResponseType + Send + Sync, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType + Send + Sync, { /// Instantiates a new dynamic client registration request. pub fn new(redirect_uris: Vec, additional_metadata: AC) -> Self { Self { client_metadata: ClientMetadata::new(redirect_uris, additional_metadata), initial_access_token: None, _phantom: PhantomData, } } /// Submits this request to the specified registration endpoint using the specified synchronous /// HTTP client. pub fn register( &self, registration_endpoint: &RegistrationUrl, http_client: &C, ) -> Result< ClientRegistrationResponse, ClientRegistrationError::Error>, > where C: SyncHttpClient, { self.prepare_registration(registration_endpoint) .and_then(|http_request| { http_client .call(http_request) .map_err(ClientRegistrationError::Request) }) .and_then(Self::register_response) } /// Submits this request to the specified registration endpoint using the specified asynchronous /// HTTP client. pub fn register_async<'c, C>( &'c self, registration_endpoint: &'c RegistrationUrl, http_client: &'c C, ) -> impl Future< Output = Result< ClientRegistrationResponse, ClientRegistrationError>::Error>, >, > + 'c where Self: 'c, C: AsyncHttpClient<'c>, { Box::pin(async move { let http_request = self.prepare_registration(registration_endpoint)?; let http_response = http_client .call(http_request) .await .map_err(ClientRegistrationError::Request)?; Self::register_response(http_response) }) } fn prepare_registration( &self, registration_endpoint: &RegistrationUrl, ) -> Result> where RE: std::error::Error + 'static, { let request_json = serde_json::to_string(self.client_metadata()) .map_err(ClientRegistrationError::Serialize)? .into_bytes(); let auth_header_opt = self.initial_access_token().map(auth_bearer); let mut request = http::Request::builder() .uri(registration_endpoint.to_string()) .method(Method::POST) .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) .header(CONTENT_TYPE, HeaderValue::from_static(MIME_TYPE_JSON)); if let Some((header, value)) = auth_header_opt { request = request.header(header, value); } request.body(request_json).map_err(|err| { ClientRegistrationError::Other(format!("failed to prepare request: {err}")) }) } fn register_response( http_response: HttpResponse, ) -> Result< ClientRegistrationResponse, ClientRegistrationError, > where RE: std::error::Error + 'static, { // TODO: check for WWW-Authenticate response header if bearer auth was used (see // https://tools.ietf.org/html/rfc6750#section-3) // TODO: other necessary response validation? check spec // Spec says that a successful response SHOULD use 201 Created, and a registration error // condition returns (no "SHOULD") 400 Bad Request. For now, only accept these two status // codes. We may need to relax the success status to improve interoperability. if http_response.status() != StatusCode::CREATED && http_response.status() != StatusCode::BAD_REQUEST { return Err(ClientRegistrationError::Response( http_response.status(), http_response.body().to_owned(), "unexpected HTTP status code".to_string(), )); } check_content_type(http_response.headers(), MIME_TYPE_JSON).map_err(|err_msg| { ClientRegistrationError::Response( http_response.status(), http_response.body().to_owned(), err_msg, ) })?; let response_body = String::from_utf8(http_response.body().to_owned()).map_err(|parse_error| { ClientRegistrationError::Other(format!( "couldn't parse response as UTF-8: {}", parse_error )) })?; if http_response.status() == StatusCode::BAD_REQUEST { let response_error: StandardErrorResponse = serde_path_to_error::deserialize( &mut serde_json::Deserializer::from_str(&response_body), ) .map_err(ClientRegistrationError::Parse)?; return Err(ClientRegistrationError::ServerResponse(response_error)); } serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str(&response_body)) .map_err(ClientRegistrationError::Parse) } /// Returns the client metadata associated with this registration request. pub fn client_metadata(&self) -> &ClientMetadata { &self.client_metadata } /// Returns the initial access token associated with this registration request. pub fn initial_access_token(&self) -> Option<&AccessToken> { self.initial_access_token.as_ref() } /// Sets the initial access token for this request. pub fn set_initial_access_token(mut self, access_token: Option) -> Self { self.initial_access_token = access_token; self } field_getters_setters![ pub self [self.client_metadata.standard_metadata] ["client metadata value"] { set_redirect_uris -> redirect_uris[Vec], set_response_types -> response_types[Option>>], set_grant_types -> grant_types[Option>], set_application_type -> application_type[Option], set_contacts -> contacts[Option>], set_client_name -> client_name[Option>], set_logo_uri -> logo_uri[Option>], set_client_uri -> client_uri[Option>], set_policy_uri -> policy_uri[Option>], set_tos_uri -> tos_uri[Option>], set_jwks_uri -> jwks_uri[Option], set_jwks -> jwks[Option>], set_sector_identifier_uri -> sector_identifier_uri[Option], set_subject_type -> subject_type[Option], set_id_token_signed_response_alg -> id_token_signed_response_alg[Option], set_id_token_encrypted_response_alg -> id_token_encrypted_response_alg[Option], set_id_token_encrypted_response_enc -> id_token_encrypted_response_enc[Option], set_userinfo_signed_response_alg -> userinfo_signed_response_alg[Option], set_userinfo_encrypted_response_alg -> userinfo_encrypted_response_alg[Option], set_userinfo_encrypted_response_enc -> userinfo_encrypted_response_enc[Option], set_request_object_signing_alg -> request_object_signing_alg[Option], set_request_object_encryption_alg -> request_object_encryption_alg[Option], set_request_object_encryption_enc -> request_object_encryption_enc[Option], set_token_endpoint_auth_method -> token_endpoint_auth_method[Option], set_token_endpoint_auth_signing_alg -> token_endpoint_auth_signing_alg[Option], set_default_max_age -> default_max_age[Option], set_require_auth_time -> require_auth_time[Option], set_default_acr_values -> default_acr_values[Option>], set_initiate_login_uri -> initiate_login_uri[Option], set_request_uris -> request_uris[Option>], } ]; /// Returns additional client metadata fields. pub fn additional_metadata(&self) -> &AC { &self.client_metadata.additional_metadata } /// Returns mutable additional client metadata fields. pub fn additional_metadata_mut(&mut self) -> &mut AC { &mut self.client_metadata.additional_metadata } } /// Trait for adding extra fields to the [`ClientRegistrationResponse`]. pub trait AdditionalClientRegistrationResponse: Debug + DeserializeOwned + Serialize {} // In order to support serde flatten, this must be an empty struct rather than an empty // tuple struct. /// Empty (default) extra [`ClientRegistrationResponse`] fields. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] pub struct EmptyAdditionalClientRegistrationResponse {} impl AdditionalClientRegistrationResponse for EmptyAdditionalClientRegistrationResponse {} /// Response to a dynamic client registration request. #[serde_as] #[skip_serializing_none] #[derive(Debug, Deserialize, Serialize)] pub struct ClientRegistrationResponse where AC: AdditionalClientMetadata, AR: AdditionalClientRegistrationResponse, AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { client_id: ClientId, client_secret: Option, registration_access_token: Option, registration_client_uri: Option, #[serde_as(as = "Option")] client_id_issued_at: Option>, #[serde_as(as = "Option")] client_secret_expires_at: Option>, #[serde(bound = "AC: AdditionalClientMetadata", flatten)] client_metadata: ClientMetadata, #[serde(bound = "AR: AdditionalClientRegistrationResponse", flatten)] additional_response: AR, } impl ClientRegistrationResponse where AC: AdditionalClientMetadata, AR: AdditionalClientRegistrationResponse, AT: ApplicationType, CA: ClientAuthMethod, G: GrantType, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, JK: JweKeyManagementAlgorithm, K: JsonWebKey, RT: ResponseType, S: SubjectIdentifierType, { /// Instantiates a new dynamic client registration response. pub fn new( client_id: ClientId, redirect_uris: Vec, additional_metadata: AC, additional_response: AR, ) -> Self { Self { client_id, client_secret: None, registration_access_token: None, registration_client_uri: None, client_id_issued_at: None, client_secret_expires_at: None, client_metadata: ClientMetadata::new(redirect_uris, additional_metadata), additional_response, } } /// Instantiates a new dynamic client registration response using the specified client metadata. pub fn from_client_metadata( client_id: ClientId, client_metadata: ClientMetadata, additional_response: AR, ) -> Self { Self { client_id, client_secret: None, registration_access_token: None, registration_client_uri: None, client_id_issued_at: None, client_secret_expires_at: None, client_metadata, additional_response, } } field_getters_setters![ pub self [self] ["response field"] { set_client_id -> client_id[ClientId], set_client_secret -> client_secret[Option], set_registration_access_token -> registration_access_token[Option], set_registration_client_uri -> registration_client_uri[Option], set_client_id_issued_at -> client_id_issued_at[Option>], set_client_secret_expires_at -> client_secret_expires_at[Option>], } ]; field_getters_setters![ pub self [self.client_metadata.standard_metadata] ["client metadata value"] { set_redirect_uris -> redirect_uris[Vec], set_response_types -> response_types[Option>>], set_grant_types -> grant_types[Option>], set_application_type -> application_type[Option], set_contacts -> contacts[Option>], set_client_name -> client_name[Option>], set_logo_uri -> logo_uri[Option>], set_client_uri -> client_uri[Option>], set_policy_uri -> policy_uri[Option>], set_tos_uri -> tos_uri[Option>], set_jwks_uri -> jwks_uri[Option], set_jwks -> jwks[Option>], set_sector_identifier_uri -> sector_identifier_uri[Option], set_subject_type -> subject_type[Option], set_id_token_signed_response_alg -> id_token_signed_response_alg[Option], set_id_token_encrypted_response_alg -> id_token_encrypted_response_alg[Option], set_id_token_encrypted_response_enc -> id_token_encrypted_response_enc[Option], set_userinfo_signed_response_alg -> userinfo_signed_response_alg[Option], set_userinfo_encrypted_response_alg -> userinfo_encrypted_response_alg[Option], set_userinfo_encrypted_response_enc -> userinfo_encrypted_response_enc[Option], set_request_object_signing_alg -> request_object_signing_alg[Option], set_request_object_encryption_alg -> request_object_encryption_alg[Option], set_request_object_encryption_enc -> request_object_encryption_enc[Option], set_token_endpoint_auth_method -> token_endpoint_auth_method[Option], set_token_endpoint_auth_signing_alg -> token_endpoint_auth_signing_alg[Option], set_default_max_age -> default_max_age[Option], set_require_auth_time -> require_auth_time[Option], set_default_acr_values -> default_acr_values[Option>], set_initiate_login_uri -> initiate_login_uri[Option], set_request_uris -> request_uris[Option>], } ]; /// Returns additional client metadata fields. pub fn additional_metadata(&self) -> &AC { &self.client_metadata.additional_metadata } /// Returns mutable additional client metadata fields. pub fn additional_metadata_mut(&mut self) -> &mut AC { &mut self.client_metadata.additional_metadata } /// Returns additional response fields. pub fn additional_response(&self) -> &AR { &self.additional_response } /// Returns mutable additional response fields. pub fn additional_response_mut(&mut self) -> &mut AR { &mut self.additional_response } } // TODO: implement client configuration endpoint request (Section 4) /// Trait representing an error returned by the dynamic client registration endpoint. pub trait RegisterErrorResponseType: ErrorResponseType + 'static {} /// Error registering a client. #[derive(Debug, Error)] #[non_exhaustive] pub enum ClientRegistrationError where RE: std::error::Error + 'static, T: RegisterErrorResponseType, { /// An unexpected error occurred. #[error("Other error: {0}")] Other(String), /// Failed to parse server response. #[error("Failed to parse server response")] Parse(#[source] serde_path_to_error::Error), /// An error occurred while sending the request or receiving the response (e.g., network /// connectivity failed). #[error("Request failed")] Request(#[source] RE), /// Server returned an invalid response. #[error("Server returned invalid response with status {0}: {2}")] Response(StatusCode, Vec, String), /// Failed to serialize client metadata. #[error("Failed to serialize client metadata")] Serialize(#[source] serde_json::Error), /// Server returned an error. #[error("Server returned error: {0}")] ServerResponse(StandardErrorResponse), } ================================================ FILE: src/registration/tests.rs ================================================ use crate::core::{ CoreApplicationType, CoreClientAuthMethod, CoreClientMetadata, CoreClientRegistrationResponse, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJwsSigningAlgorithm, CoreResponseType, CoreSubjectIdentifierType, }; use crate::jwt::tests::TEST_RSA_PUB_KEY; use crate::{ AuthenticationContextClass, ClientConfigUrl, ClientContactEmail, ClientName, ClientUrl, JsonWebKeySet, JsonWebKeySetUrl, LanguageTag, LogoUrl, PolicyUrl, RequestUrl, ResponseTypes, SectorIdentifierUrl, ToSUrl, }; use crate::{ClientId, RedirectUrl}; use chrono::{TimeZone, Utc}; use itertools::sorted; use std::time::Duration; #[test] fn test_metadata_serialization() { // `jwks_uri` and `jwks` aren't supposed to be used together, but this test is just for // serialization/deserialization. let json_response = format!("{{ \"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"], \"response_types\": [\"code\", \"code token id_token\"], \"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \ \"password\", \"refresh_token\"], \"application_type\": \"web\", \"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"], \"client_name\": \"Example\", \"client_name#es\": \"Ejemplo\", \"logo_uri\": \"https://example.com/logo.png\", \"logo_uri#fr\": \"https://example.com/logo-fr.png\", \"client_uri\": \"https://example.com/client-app\", \"client_uri#de\": \"https://example.com/client-app-de\", \"policy_uri\": \"https://example.com/policy\", \"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\", \"tos_uri\": \"https://example.com/tos\", \"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\", \"jwks_uri\": \"https://example.com/jwks\", \"jwks\": {{\"keys\": [{}]}}, \"sector_identifier_uri\": \"https://example.com/sector\", \"subject_type\": \"pairwise\", \"id_token_signed_response_alg\": \"HS256\", \"id_token_encrypted_response_alg\": \"RSA1_5\", \"id_token_encrypted_response_enc\": \"A128CBC-HS256\", \"userinfo_signed_response_alg\": \"RS384\", \"userinfo_encrypted_response_alg\": \"RSA-OAEP\", \"userinfo_encrypted_response_enc\": \"A256CBC-HS512\", \"request_object_signing_alg\": \"ES512\", \"request_object_encryption_alg\": \"ECDH-ES+A128KW\", \"request_object_encryption_enc\": \"A256GCM\", \"token_endpoint_auth_method\": \"client_secret_basic\", \"token_endpoint_auth_signing_alg\": \"PS512\", \"default_max_age\": 3600, \"require_auth_time\": true, \"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \ \"urn:mace:incommon:iap:bronze\"], \"initiate_login_uri\": \"https://example.com/login\", \"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"] }}", TEST_RSA_PUB_KEY); let client_metadata: CoreClientMetadata = serde_json::from_str(&json_response).unwrap(); assert_eq!( *client_metadata.redirect_uris(), vec![ RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(), RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(), ] ); assert_eq!( *client_metadata.response_types().unwrap(), vec![ ResponseTypes::new(vec![CoreResponseType::Code]), ResponseTypes::new(vec![ CoreResponseType::Code, CoreResponseType::Token, CoreResponseType::IdToken, ]), ] ); assert_eq!( client_metadata.grant_types().unwrap(), &vec![ CoreGrantType::AuthorizationCode, CoreGrantType::ClientCredentials, CoreGrantType::Implicit, CoreGrantType::Password, CoreGrantType::RefreshToken, ] ); assert_eq!( *client_metadata.application_type().unwrap(), CoreApplicationType::Web ); assert_eq!( *client_metadata.contacts().unwrap(), vec![ ClientContactEmail::new("user@example.com".to_string()), ClientContactEmail::new("admin@openidconnect.local".to_string()), ] ); assert_eq!( sorted(client_metadata.client_name().unwrap().clone()) .collect::, ClientName)>>(), vec![ (None, ClientName::new("Example".to_string())), ( Some(LanguageTag::new("es".to_string())), ClientName::new("Ejemplo".to_string()), ), ] ); assert_eq!( sorted(client_metadata.logo_uri().unwrap().clone()) .collect::, LogoUrl)>>(), vec![ ( None, LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(), ), ( Some(LanguageTag::new("fr".to_string())), LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(), ), ] ); assert_eq!( sorted(client_metadata.client_uri().unwrap().clone()) .collect::, ClientUrl)>>(), vec![ ( None, ClientUrl::new("https://example.com/client-app".to_string()).unwrap(), ), ( Some(LanguageTag::new("de".to_string())), ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(), ), ] ); assert_eq!( sorted(client_metadata.policy_uri().unwrap().clone()) .collect::, PolicyUrl)>>(), vec![ ( None, PolicyUrl::new("https://example.com/policy".to_string()).unwrap(), ), ( Some(LanguageTag::new("sr-Latn".to_string())), PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(), ), ] ); assert_eq!( sorted(client_metadata.tos_uri().unwrap().clone()) .collect::, ToSUrl)>>(), vec![ ( None, ToSUrl::new("https://example.com/tos".to_string()).unwrap(), ), ( Some(LanguageTag::new("sr-Cyrl".to_string())), ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(), ), ] ); assert_eq!( *client_metadata.jwks_uri().unwrap(), JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap() ); assert_eq!( client_metadata.jwks(), Some(&JsonWebKeySet::new(vec![serde_json::from_str( TEST_RSA_PUB_KEY ) .unwrap()],)) ); assert_eq!( *client_metadata.sector_identifier_uri().unwrap(), SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() ); assert_eq!( *client_metadata.subject_type().unwrap(), CoreSubjectIdentifierType::Pairwise ); assert_eq!( *client_metadata.id_token_signed_response_alg().unwrap(), CoreJwsSigningAlgorithm::HmacSha256 ); assert_eq!( *client_metadata.id_token_encrypted_response_alg().unwrap(), CoreJweKeyManagementAlgorithm::RsaPkcs1V15 ); assert_eq!( *client_metadata.id_token_encrypted_response_enc().unwrap(), CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 ); assert_eq!( *client_metadata.userinfo_signed_response_alg().unwrap(), CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 ); assert_eq!( *client_metadata.userinfo_encrypted_response_alg().unwrap(), CoreJweKeyManagementAlgorithm::RsaOaep ); assert_eq!( *client_metadata.userinfo_encrypted_response_enc().unwrap(), CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512 ); assert_eq!( *client_metadata.request_object_signing_alg().unwrap(), CoreJwsSigningAlgorithm::EcdsaP521Sha512 ); assert_eq!( *client_metadata.request_object_encryption_alg().unwrap(), CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128 ); assert_eq!( *client_metadata.request_object_encryption_enc().unwrap(), CoreJweContentEncryptionAlgorithm::Aes256Gcm ); assert_eq!( *client_metadata.token_endpoint_auth_method().unwrap(), CoreClientAuthMethod::ClientSecretBasic ); assert_eq!( *client_metadata.token_endpoint_auth_signing_alg().unwrap(), CoreJwsSigningAlgorithm::RsaSsaPssSha512 ); assert_eq!( *client_metadata.default_max_age().unwrap(), Duration::from_secs(3600) ); assert!(client_metadata.require_auth_time().unwrap()); assert_eq!( *client_metadata.default_acr_values().unwrap(), vec![ AuthenticationContextClass::new("0".to_string()), AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()), AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()), ] ); assert_eq!( *client_metadata.sector_identifier_uri().unwrap(), SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() ); assert_eq!( *client_metadata.request_uris().unwrap(), vec![ RequestUrl::new("https://example.com/request-1".to_string()).unwrap(), RequestUrl::new("https://example.com/request-2".to_string()).unwrap(), ] ); let serialized_json = serde_json::to_string(&client_metadata).unwrap(); assert_eq!( client_metadata, serde_json::from_str(&serialized_json).unwrap() ); } #[test] fn test_metadata_serialization_minimal() { let json_response = "{\"redirect_uris\": [\"https://example.com/redirect-1\"]}"; let client_metadata: CoreClientMetadata = serde_json::from_str(json_response).unwrap(); assert_eq!( *client_metadata.redirect_uris(), vec![RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(),] ); assert_eq!(client_metadata.response_types(), None); assert_eq!(client_metadata.grant_types(), None); assert_eq!(client_metadata.application_type(), None); assert_eq!(client_metadata.contacts(), None); assert_eq!(client_metadata.client_name(), None); assert_eq!(client_metadata.logo_uri(), None); assert_eq!(client_metadata.client_uri(), None); assert_eq!(client_metadata.policy_uri(), None); assert_eq!(client_metadata.tos_uri(), None); assert_eq!(client_metadata.jwks_uri(), None); assert_eq!(client_metadata.jwks(), None); assert_eq!(client_metadata.sector_identifier_uri(), None); assert_eq!(client_metadata.subject_type(), None); assert_eq!(client_metadata.id_token_signed_response_alg(), None); assert_eq!(client_metadata.id_token_encrypted_response_alg(), None); assert_eq!(client_metadata.id_token_encrypted_response_enc(), None); assert_eq!(client_metadata.userinfo_signed_response_alg(), None); assert_eq!(client_metadata.userinfo_encrypted_response_alg(), None); assert_eq!(client_metadata.userinfo_encrypted_response_enc(), None); assert_eq!(client_metadata.request_object_signing_alg(), None); assert_eq!(client_metadata.request_object_encryption_alg(), None); assert_eq!(client_metadata.request_object_encryption_enc(), None); assert_eq!(client_metadata.token_endpoint_auth_method(), None); assert_eq!(client_metadata.token_endpoint_auth_signing_alg(), None); assert_eq!(client_metadata.default_max_age(), None); assert_eq!(client_metadata.require_auth_time(), None); assert_eq!(client_metadata.default_acr_values(), None); assert_eq!(client_metadata.sector_identifier_uri(), None); assert_eq!(client_metadata.request_uris(), None); let serialized_json = serde_json::to_string(&client_metadata).unwrap(); assert_eq!( client_metadata, serde_json::from_str(&serialized_json).unwrap() ); } #[test] fn test_response_serialization() { let json_response = format!("{{ \"client_id\": \"abcdefgh\", \"client_secret\": \"shhhh\", \"registration_access_token\": \"use_me_to_update_registration\", \"registration_client_uri\": \"https://example-provider.com/registration\", \"client_id_issued_at\": 1523953306, \"client_secret_expires_at\": 1526545306, \"redirect_uris\": [\"https://example.com/redirect-1\", \"https://example.com/redirect-2\"], \"response_types\": [\"code\", \"code token id_token\"], \"grant_types\": [\"authorization_code\", \"client_credentials\", \"implicit\", \ \"password\", \"refresh_token\"], \"application_type\": \"web\", \"contacts\": [\"user@example.com\", \"admin@openidconnect.local\"], \"client_name\": \"Example\", \"client_name#es\": \"Ejemplo\", \"logo_uri\": \"https://example.com/logo.png\", \"logo_uri#fr\": \"https://example.com/logo-fr.png\", \"client_uri\": \"https://example.com/client-app\", \"client_uri#de\": \"https://example.com/client-app-de\", \"policy_uri\": \"https://example.com/policy\", \"policy_uri#sr-Latn\": \"https://example.com/policy-sr-latin\", \"tos_uri\": \"https://example.com/tos\", \"tos_uri#sr-Cyrl\": \"https://example.com/tos-sr-cyrl\", \"jwks_uri\": \"https://example.com/jwks\", \"jwks\": {{\"keys\": [{}]}}, \"sector_identifier_uri\": \"https://example.com/sector\", \"subject_type\": \"pairwise\", \"id_token_signed_response_alg\": \"HS256\", \"id_token_encrypted_response_alg\": \"RSA1_5\", \"id_token_encrypted_response_enc\": \"A128CBC-HS256\", \"userinfo_signed_response_alg\": \"RS384\", \"userinfo_encrypted_response_alg\": \"RSA-OAEP\", \"userinfo_encrypted_response_enc\": \"A256CBC-HS512\", \"request_object_signing_alg\": \"ES512\", \"request_object_encryption_alg\": \"ECDH-ES+A128KW\", \"request_object_encryption_enc\": \"A256GCM\", \"token_endpoint_auth_method\": \"client_secret_basic\", \"token_endpoint_auth_signing_alg\": \"PS512\", \"default_max_age\": 3600, \"require_auth_time\": true, \"default_acr_values\": [\"0\", \"urn:mace:incommon:iap:silver\", \ \"urn:mace:incommon:iap:bronze\"], \"initiate_login_uri\": \"https://example.com/login\", \"request_uris\": [\"https://example.com/request-1\", \"https://example.com/request-2\"] }}", TEST_RSA_PUB_KEY); let registration_response: CoreClientRegistrationResponse = serde_json::from_str(&json_response).unwrap(); assert_eq!( *registration_response.client_id(), ClientId::new("abcdefgh".to_string()) ); assert_eq!( *registration_response.client_secret().unwrap().secret(), "shhhh" ); assert_eq!( *registration_response .registration_access_token() .unwrap() .secret(), "use_me_to_update_registration", ); assert_eq!( *registration_response.registration_client_uri().unwrap(), ClientConfigUrl::new("https://example-provider.com/registration".to_string()).unwrap() ); assert_eq!( registration_response.client_id_issued_at().unwrap(), Utc.timestamp_opt(1523953306, 0) .single() .expect("valid timestamp") ); assert_eq!( registration_response.client_secret_expires_at().unwrap(), Utc.timestamp_opt(1526545306, 0) .single() .expect("valid timestamp") ); assert_eq!( *registration_response.redirect_uris(), vec![ RedirectUrl::new("https://example.com/redirect-1".to_string()).unwrap(), RedirectUrl::new("https://example.com/redirect-2".to_string()).unwrap(), ] ); assert_eq!( *registration_response.response_types().unwrap(), vec![ ResponseTypes::new(vec![CoreResponseType::Code]), ResponseTypes::new(vec![ CoreResponseType::Code, CoreResponseType::Token, CoreResponseType::IdToken, ]), ] ); assert_eq!( registration_response.grant_types().unwrap(), &vec![ CoreGrantType::AuthorizationCode, CoreGrantType::ClientCredentials, CoreGrantType::Implicit, CoreGrantType::Password, CoreGrantType::RefreshToken, ] ); assert_eq!( *registration_response.application_type().unwrap(), CoreApplicationType::Web ); assert_eq!( *registration_response.contacts().unwrap(), vec![ ClientContactEmail::new("user@example.com".to_string()), ClientContactEmail::new("admin@openidconnect.local".to_string()), ] ); assert_eq!( sorted(registration_response.client_name().unwrap().clone()) .collect::, ClientName)>>(), vec![ (None, ClientName::new("Example".to_string())), ( Some(LanguageTag::new("es".to_string())), ClientName::new("Ejemplo".to_string()), ), ] ); assert_eq!( sorted(registration_response.logo_uri().unwrap().clone()) .collect::, LogoUrl)>>(), vec![ ( None, LogoUrl::new("https://example.com/logo.png".to_string()).unwrap(), ), ( Some(LanguageTag::new("fr".to_string())), LogoUrl::new("https://example.com/logo-fr.png".to_string()).unwrap(), ), ] ); assert_eq!( sorted(registration_response.client_uri().unwrap().clone()) .collect::, ClientUrl)>>(), vec![ ( None, ClientUrl::new("https://example.com/client-app".to_string()).unwrap(), ), ( Some(LanguageTag::new("de".to_string())), ClientUrl::new("https://example.com/client-app-de".to_string()).unwrap(), ), ] ); assert_eq!( sorted(registration_response.policy_uri().unwrap().clone()) .collect::, PolicyUrl)>>(), vec![ ( None, PolicyUrl::new("https://example.com/policy".to_string()).unwrap(), ), ( Some(LanguageTag::new("sr-Latn".to_string())), PolicyUrl::new("https://example.com/policy-sr-latin".to_string()).unwrap(), ), ] ); assert_eq!( sorted(registration_response.tos_uri().unwrap().clone()) .collect::, ToSUrl)>>(), vec![ ( None, ToSUrl::new("https://example.com/tos".to_string()).unwrap(), ), ( Some(LanguageTag::new("sr-Cyrl".to_string())), ToSUrl::new("https://example.com/tos-sr-cyrl".to_string()).unwrap(), ), ] ); assert_eq!( *registration_response.jwks_uri().unwrap(), JsonWebKeySetUrl::new("https://example.com/jwks".to_string()).unwrap() ); assert_eq!( registration_response.jwks(), Some(&JsonWebKeySet::new(vec![serde_json::from_str( TEST_RSA_PUB_KEY ) .unwrap()],)), ); assert_eq!( *registration_response.sector_identifier_uri().unwrap(), SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() ); assert_eq!( *registration_response.subject_type().unwrap(), CoreSubjectIdentifierType::Pairwise ); assert_eq!( *registration_response .id_token_signed_response_alg() .unwrap(), CoreJwsSigningAlgorithm::HmacSha256 ); assert_eq!( *registration_response .id_token_encrypted_response_alg() .unwrap(), CoreJweKeyManagementAlgorithm::RsaPkcs1V15 ); assert_eq!( *registration_response .id_token_encrypted_response_enc() .unwrap(), CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 ); assert_eq!( *registration_response .userinfo_signed_response_alg() .unwrap(), CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha384 ); assert_eq!( *registration_response .userinfo_encrypted_response_alg() .unwrap(), CoreJweKeyManagementAlgorithm::RsaOaep ); assert_eq!( *registration_response .userinfo_encrypted_response_enc() .unwrap(), CoreJweContentEncryptionAlgorithm::Aes256CbcHmacSha512 ); assert_eq!( *registration_response.request_object_signing_alg().unwrap(), CoreJwsSigningAlgorithm::EcdsaP521Sha512 ); assert_eq!( *registration_response .request_object_encryption_alg() .unwrap(), CoreJweKeyManagementAlgorithm::EcdhEsAesKeyWrap128 ); assert_eq!( *registration_response .request_object_encryption_enc() .unwrap(), CoreJweContentEncryptionAlgorithm::Aes256Gcm ); assert_eq!( *registration_response.token_endpoint_auth_method().unwrap(), CoreClientAuthMethod::ClientSecretBasic ); assert_eq!( *registration_response .token_endpoint_auth_signing_alg() .unwrap(), CoreJwsSigningAlgorithm::RsaSsaPssSha512 ); assert_eq!( *registration_response.default_max_age().unwrap(), Duration::from_secs(3600) ); assert!(registration_response.require_auth_time().unwrap()); assert_eq!( *registration_response.default_acr_values().unwrap(), vec![ AuthenticationContextClass::new("0".to_string()), AuthenticationContextClass::new("urn:mace:incommon:iap:silver".to_string()), AuthenticationContextClass::new("urn:mace:incommon:iap:bronze".to_string()), ] ); assert_eq!( *registration_response.sector_identifier_uri().unwrap(), SectorIdentifierUrl::new("https://example.com/sector".to_string()).unwrap() ); assert_eq!( *registration_response.request_uris().unwrap(), vec![ RequestUrl::new("https://example.com/request-1".to_string()).unwrap(), RequestUrl::new("https://example.com/request-2".to_string()).unwrap(), ] ); let serialized_json = serde_json::to_string(®istration_response).unwrap(); let deserialized: CoreClientRegistrationResponse = serde_json::from_str(&serialized_json).unwrap(); assert_eq!(registration_response.client_id, deserialized.client_id); assert_eq!( registration_response.client_secret.unwrap().secret(), deserialized.client_secret.unwrap().secret(), ); assert_eq!( registration_response .registration_access_token .unwrap() .secret(), deserialized.registration_access_token.unwrap().secret(), ); assert_eq!( registration_response.registration_client_uri, deserialized.registration_client_uri, ); assert_eq!( registration_response.client_id_issued_at, deserialized.client_id_issued_at, ); assert_eq!( registration_response.client_secret_expires_at, deserialized.client_secret_expires_at, ); assert_eq!( registration_response.client_metadata, deserialized.client_metadata, ); assert_eq!( registration_response.additional_response, deserialized.additional_response, ); } ================================================ FILE: src/token.rs ================================================ use crate::{ AdditionalClaims, ExtraTokenFields, GenderClaim, IdToken, IdTokenFields, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, OAuth2TokenResponse, StandardTokenResponse, TokenType, }; /// Extends the base OAuth2 token response with an ID token. pub trait TokenResponse: OAuth2TokenResponse where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { /// Returns the ID token provided by the token response. /// /// OpenID Connect authorization servers should always return this field, but it is optional /// to allow for interoperability with authorization servers that only support OAuth2. fn id_token(&self) -> Option<&IdToken>; } impl TokenResponse for StandardTokenResponse, TT> where AC: AdditionalClaims, EF: ExtraTokenFields, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, TT: TokenType, { fn id_token(&self) -> Option<&IdToken> { self.extra_fields().id_token() } } ================================================ FILE: src/types/jwk.rs ================================================ use crate::{SignatureVerificationError, SigningError}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::hash::Hash; new_type![ /// ID of a JSON Web Key. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] JsonWebKeyId(String) ]; /// JSON Web Key. pub trait JsonWebKey: Clone + Debug + DeserializeOwned + Serialize + 'static { /// Allowed key usage. type KeyUse: JsonWebKeyUse; /// JSON Web Signature (JWS) algorithm. type SigningAlgorithm: JwsSigningAlgorithm; /// Returns the key ID, or `None` if no key ID is specified. fn key_id(&self) -> Option<&JsonWebKeyId>; /// Returns the key type (e.g., RSA). fn key_type(&self) -> &::KeyType; /// Returns the allowed key usage (e.g., signing or encryption), or `None` if no usage is /// specified. fn key_use(&self) -> Option<&Self::KeyUse>; /// Returns the algorithm (e.g. ES512) this key must be used with, or `Unspecified` if /// no algorithm constraint was given, or unsupported if the algorithm is not for signing. /// /// It's not sufficient to tell whether a key can be used for signing, as key use also has to be validated. fn signing_alg(&self) -> JsonWebKeyAlgorithm<&Self::SigningAlgorithm>; /// Initializes a new symmetric key or shared signing secret from the specified raw bytes. fn new_symmetric(key: Vec) -> Self; /// Verifies the given `signature` using the given signature algorithm (`signature_alg`) over /// the given `message`. /// /// Returns `Ok` if the signature is valid, or an `Err` otherwise. fn verify_signature( &self, signature_alg: &Self::SigningAlgorithm, message: &[u8], signature: &[u8], ) -> Result<(), SignatureVerificationError>; /// Hashes the given `bytes` using the hash function associated with the specified signing /// algorithm and returns the hashed bytes. /// /// Certain signing algorithms (e.g., `EdDSA`) use different hash functions depending on the /// type of key (e.g., whether the `Ed25519` or `Ed448` curve is used), so this method is /// implemented on the corresponding public key instead of the [`JwsSigningAlgorithm`] trait /// to allow the implementation to determine the proper hash function to use. /// If hashing fails or this key/signing algorithm does not have an associated hash function, an /// `Err` is returned with a string describing the cause of the error. An error is also returned /// if the specified signature algorithm is incompatible with this key (e.g., passing `EdDSA` /// with an RSA key). fn hash_bytes(&self, bytes: &[u8], alg: &Self::SigningAlgorithm) -> Result, String>; } /// Encodes a JWK key's alg field compatibility with either signing or encryption operations. #[derive(Debug)] pub enum JsonWebKeyAlgorithm { /// the alg field allows this kind of operation to be performed with this algorithm only Algorithm(A), /// there is no alg field Unspecified, /// the alg field's algorithm is incompatible with this kind of operation Unsupported, } /// Private or symmetric key for signing. pub trait PrivateSigningKey { /// Corresponding type of JSON Web Key used for verifying signatures produced by this key. type VerificationKey: JsonWebKey; /// Signs the given `message` using the given signature algorithm. fn sign( &self, signature_alg: &::SigningAlgorithm, message: &[u8], ) -> Result, SigningError>; /// Converts this key to a JSON Web Key that can be used for verifying signatures. fn as_verification_key(&self) -> Self::VerificationKey; } /// Key type (e.g., RSA). pub trait JsonWebKeyType: Clone + Debug + DeserializeOwned + PartialEq + Serialize + 'static { } /// Allowed key usage. pub trait JsonWebKeyUse: Debug + DeserializeOwned + Serialize + 'static { /// Returns true if the associated key may be used for digital signatures, or false otherwise. fn allows_signature(&self) -> bool; /// Returns true if the associated key may be used for encryption, or false otherwise. fn allows_encryption(&self) -> bool; } /// JSON Web Encryption (JWE) content encryption algorithm. pub trait JweContentEncryptionAlgorithm: Clone + Debug + DeserializeOwned + Serialize + 'static { /// Key type (e.g., RSA). type KeyType: JsonWebKeyType; /// Returns the type of key required to use this encryption algorithm. fn key_type(&self) -> Result; } /// JSON Web Encryption (JWE) key management algorithm. pub trait JweKeyManagementAlgorithm: Debug + DeserializeOwned + Serialize + 'static { // TODO: add a key_type() method } /// JSON Web Signature (JWS) algorithm. pub trait JwsSigningAlgorithm: Clone + Debug + DeserializeOwned + Eq + Hash + PartialEq + Serialize + 'static { /// Key type (e.g., RSA). type KeyType: JsonWebKeyType; /// Returns the type of key required to use this signature algorithm, or `None` if this /// algorithm does not require a key. fn key_type(&self) -> Option; /// Returns true if the signature algorithm uses a shared secret (symmetric key). fn uses_shared_secret(&self) -> bool; /// Returns the RS256 algorithm. /// /// This is the default algorithm for OpenID Connect ID tokens and must be supported by all /// implementations. fn rsa_sha_256() -> Self; } ================================================ FILE: src/types/jwks.rs ================================================ use crate::http_utils::{check_content_type, MIME_TYPE_JSON, MIME_TYPE_JWKS}; use crate::types::jwk::{JsonWebKey, JsonWebKeyId, JwsSigningAlgorithm}; use crate::{ AsyncHttpClient, DiscoveryError, HttpRequest, HttpResponse, JsonWebKeyUse, SyncHttpClient, }; use http::header::ACCEPT; use http::{HeaderValue, Method, StatusCode}; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, VecSkipError}; use std::future::Future; new_url_type![ /// JSON Web Key Set URL. JsonWebKeySetUrl ]; /// JSON Web Key Set. #[serde_as] #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct JsonWebKeySet where K: JsonWebKey, { // FIXME: write a test that ensures duplicate object member names cause an error // (see https://tools.ietf.org/html/rfc7517#section-5) #[serde(bound = "K: JsonWebKey")] // Ignores invalid keys rather than failing. That way, clients can function using the keys that // they do understand, which is fine if they only ever get JWTs signed with those keys. #[serde_as(as = "VecSkipError<_>")] keys: Vec, } /// Checks whether a JWK key can be used with a given signing algorithm. pub(crate) fn check_key_compatibility( key: &K, signing_algorithm: &K::SigningAlgorithm, ) -> Result<(), &'static str> where K: JsonWebKey, { // if this key isn't suitable for signing if let Some(use_) = key.key_use() { if !use_.allows_signature() { return Err("key usage not permitted for digital signatures"); } } // if this key doesn't have the right key type if signing_algorithm.key_type().as_ref() != Some(key.key_type()) { return Err("key type does not match signature algorithm"); } match key.signing_alg() { // if no specific algorithm is mandated, any will do crate::JsonWebKeyAlgorithm::Unspecified => Ok(()), crate::JsonWebKeyAlgorithm::Unsupported => Err("key algorithm is not a signing algorithm"), crate::JsonWebKeyAlgorithm::Algorithm(key_alg) if key_alg == signing_algorithm => Ok(()), crate::JsonWebKeyAlgorithm::Algorithm(_) => Err("incompatible key algorithm"), } } impl JsonWebKeySet where K: JsonWebKey, { /// Create a new JSON Web Key Set. pub fn new(keys: Vec) -> Self { Self { keys } } /// Return a list of suitable keys, given a key ID and signature algorithm pub(crate) fn filter_keys( &self, key_id: Option<&JsonWebKeyId>, signature_alg: &K::SigningAlgorithm, ) -> Vec<&K> { self.keys() .iter() .filter(|key| // Either the JWT doesn't include a 'kid' (in which case any 'kid' // is acceptable), or the 'kid' matches the key's ID. if key_id.is_some() && key_id != key.key_id() { false } else { check_key_compatibility(*key, signature_alg).is_ok() } ) .collect() } /// Fetch a remote JSON Web Key Set from the specified `url` using the given `http_client` /// (e.g., [`reqwest::blocking::Client`](crate::reqwest::blocking::Client) or /// [`CurlHttpClient`](crate::CurlHttpClient)). pub fn fetch( url: &JsonWebKeySetUrl, http_client: &C, ) -> Result::Error>> where C: SyncHttpClient, { http_client .call(Self::fetch_request(url).map_err(|err| { DiscoveryError::Other(format!("failed to prepare request: {err}")) })?) .map_err(DiscoveryError::Request) .and_then(Self::fetch_response) } /// Fetch a remote JSON Web Key Set from the specified `url` using the given async `http_client` /// (e.g., [`reqwest::Client`](crate::reqwest::Client)). pub fn fetch_async<'c, C>( url: &JsonWebKeySetUrl, http_client: &'c C, ) -> impl Future>::Error>>> + 'c where Self: 'c, C: AsyncHttpClient<'c>, { let fetch_request = Self::fetch_request(url) .map_err(|err| DiscoveryError::Other(format!("failed to prepare request: {err}"))); Box::pin(async move { http_client .call(fetch_request?) .await .map_err(DiscoveryError::Request) .and_then(Self::fetch_response) }) } fn fetch_request(url: &JsonWebKeySetUrl) -> Result { http::Request::builder() .uri(url.to_string()) .method(Method::GET) .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) .body(Vec::new()) } fn fetch_response(http_response: HttpResponse) -> Result> where RE: std::error::Error + 'static, { if http_response.status() != StatusCode::OK { return Err(DiscoveryError::Response( http_response.status(), http_response.body().to_owned(), format!("HTTP status code {}", http_response.status()), )); } check_content_type(http_response.headers(), MIME_TYPE_JSON) .or_else(|err| { check_content_type(http_response.headers(), MIME_TYPE_JWKS).map_err(|_| err) }) .map_err(|err_msg| { DiscoveryError::Response( http_response.status(), http_response.body().to_owned(), err_msg, ) })?; serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice( http_response.body(), )) .map_err(DiscoveryError::Parse) } /// Return the keys in this JSON Web Key Set. pub fn keys(&self) -> &Vec { &self.keys } } impl Clone for JsonWebKeySet where K: JsonWebKey, { fn clone(&self) -> Self { Self::new(self.keys.clone()) } } impl Default for JsonWebKeySet where K: JsonWebKey, { fn default() -> Self { Self::new(Vec::new()) } } ================================================ FILE: src/types/localized.rs ================================================ use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Display; new_type![ /// Language tag adhering to RFC 5646 (e.g., `fr` or `fr-CA`). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] LanguageTag(String) ]; impl AsRef for LanguageTag { fn as_ref(&self) -> &str { self } } impl Display for LanguageTag { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{}", self.as_ref()) } } pub(crate) fn split_language_tag_key(key: &str) -> (&str, Option) { let mut lang_tag_sep = key.splitn(2, '#'); // String::splitn(2) always returns at least one element. let field_name = lang_tag_sep.next().unwrap(); let language_tag = lang_tag_sep .next() .filter(|language_tag| !language_tag.is_empty()) .map(|language_tag| LanguageTag::new(language_tag.to_string())); (field_name, language_tag) } pub(crate) fn join_language_tag_key<'a>( field_name: &'a str, language_tag: Option<&LanguageTag>, ) -> Cow<'a, str> { if let Some(language_tag) = language_tag { Cow::Owned(format!("{field_name}#{language_tag}")) } else { Cow::Borrowed(field_name) } } /// A [locale-aware](https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsLanguages) /// claim. /// /// This structure associates one more `Option` locales with the corresponding /// claims values. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LocalizedClaim(HashMap, Option); impl LocalizedClaim { /// Initialize an empty claim. pub fn new() -> Self { Self::default() } /// Returns true if the claim contains a value for the specified locale. pub fn contains_key(&self, locale: Option<&LanguageTag>) -> bool { if let Some(l) = locale { self.0.contains_key(l) } else { self.1.is_some() } } /// Returns the entry for the specified locale or `None` if there is no such entry. pub fn get(&self, locale: Option<&LanguageTag>) -> Option<&T> { if let Some(l) = locale { self.0.get(l) } else { self.1.as_ref() } } /// Returns an iterator over the locales and claim value entries. pub fn iter(&self) -> impl Iterator, &T)> { self.1 .iter() .map(|value| (None, value)) .chain(self.0.iter().map(|(locale, value)| (Some(locale), value))) } /// Inserts or updates an entry for the specified locale. /// /// Returns the current value associated with the given locale, or `None` if there is no /// such entry. pub fn insert(&mut self, locale: Option, value: T) -> Option { if let Some(l) = locale { self.0.insert(l, value) } else { self.1.replace(value) } } /// Removes an entry for the specified locale. /// /// Returns the current value associated with the given locale, or `None` if there is no /// such entry. pub fn remove(&mut self, locale: Option<&LanguageTag>) -> Option { if let Some(l) = locale { self.0.remove(l) } else { self.1.take() } } } impl LocalizedClaim> { pub(crate) fn flatten_or_none(self) -> Option> { let flattened_tagged = self .0 .into_iter() .filter_map(|(k, v)| v.map(|v| (k, v))) .collect::>(); let flattened_default = self.1.flatten(); if flattened_tagged.is_empty() && flattened_default.is_none() { None } else { Some(LocalizedClaim(flattened_tagged, flattened_default)) } } } impl Default for LocalizedClaim { fn default() -> Self { Self(HashMap::new(), None) } } impl From for LocalizedClaim { fn from(default: T) -> Self { Self(HashMap::new(), Some(default)) } } impl FromIterator<(Option, T)> for LocalizedClaim { fn from_iter, T)>>(iter: I) -> Self { let mut temp: HashMap, T> = iter.into_iter().collect(); let default = temp.remove(&None); Self( temp.into_iter() .filter_map(|(locale, value)| locale.map(|l| (l, value))) .collect(), default, ) } } impl IntoIterator for LocalizedClaim where T: 'static, { type Item = as Iterator>::Item; type IntoIter = LocalizedClaimIterator; fn into_iter(self) -> Self::IntoIter { LocalizedClaimIterator { inner: Box::new( self.1.into_iter().map(|value| (None, value)).chain( self.0 .into_iter() .map(|(locale, value)| (Some(locale), value)), ), ), } } } /// Owned iterator over a LocalizedClaim. pub struct LocalizedClaimIterator { inner: Box, T)>>, } impl Iterator for LocalizedClaimIterator { type Item = (Option, T); fn next(&mut self) -> Option { self.inner.next() } } ================================================ FILE: src/types/mod.rs ================================================ use crate::types::jwk::JsonWebKey; use crate::{AccessToken, AuthorizationCode}; use base64::prelude::BASE64_URL_SAFE_NO_PAD; use base64::Engine; use oauth2::helpers::deserialize_space_delimited_vec; use rand::{thread_rng, Rng}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; use std::fmt::Debug; use std::hash::Hash; use std::ops::Deref; pub(crate) mod jwk; pub(crate) mod jwks; pub(crate) mod localized; #[cfg(test)] mod tests; /// Client application type. pub trait ApplicationType: Debug + DeserializeOwned + Serialize + 'static {} /// How the Authorization Server displays the authentication and consent user interface pages to /// the End-User. pub trait AuthDisplay: AsRef + Debug + DeserializeOwned + Serialize + 'static {} /// Whether the Authorization Server should prompt the End-User for reauthentication and consent. pub trait AuthPrompt: AsRef + 'static {} /// Claim name. pub trait ClaimName: Debug + DeserializeOwned + Serialize + 'static {} /// Claim type (e.g., normal, aggregated, or distributed). pub trait ClaimType: Debug + DeserializeOwned + Serialize + 'static {} /// Client authentication method. pub trait ClientAuthMethod: Debug + DeserializeOwned + Serialize + 'static {} /// Grant type. pub trait GrantType: Debug + DeserializeOwned + Serialize + 'static {} /// Error signing a message. #[derive(Clone, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub enum SigningError { /// Failed to sign the message using the given key and parameters. #[error("Crypto error")] CryptoError, /// Unsupported signature algorithm. #[error("Unsupported signature algorithm: {0}")] UnsupportedAlg(String), /// An unexpected error occurred. #[error("Other error: {0}")] Other(String), } /// Response mode indicating how the OpenID Connect Provider should return the Authorization /// Response to the Relying Party (client). pub trait ResponseMode: Debug + DeserializeOwned + Serialize + 'static {} /// Response type indicating the desired authorization processing flow, including what /// parameters are returned from the endpoints used. pub trait ResponseType: AsRef + Debug + DeserializeOwned + Serialize + 'static { /// Converts this OpenID Connect response type to an [`oauth2::ResponseType`] used by the /// underlying [`oauth2`] crate. fn to_oauth2(&self) -> oauth2::ResponseType; } /// Subject identifier type returned by an OpenID Connect Provider to uniquely identify its users. pub trait SubjectIdentifierType: Debug + DeserializeOwned + Serialize + 'static {} new_type![ /// Set of authentication methods or procedures that are considered to be equivalent to each /// other in a particular context. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AuthenticationContextClass(String) ]; impl AsRef for AuthenticationContextClass { fn as_ref(&self) -> &str { self } } new_type![ /// Identifier for an authentication method (e.g., `password` or `totp`). /// /// Defining specific AMR identifiers is beyond the scope of the OpenID Connect Core spec. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AuthenticationMethodReference(String) ]; new_type![ /// Access token hash. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AccessTokenHash(String) impl { /// Initialize a new access token hash from an [`AccessToken`] and signature algorithm. pub fn from_token( access_token: &AccessToken, alg: &K::SigningAlgorithm, key: &K, ) -> Result where K: JsonWebKey, { key.hash_bytes(access_token.secret().as_bytes(), alg) .map(|hash| Self::new(BASE64_URL_SAFE_NO_PAD.encode(&hash[0..hash.len() / 2]))) .map_err(SigningError::UnsupportedAlg) } } ]; new_type![ /// Country portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AddressCountry(String) ]; new_type![ /// Locality portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AddressLocality(String) ]; new_type![ /// Postal code portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AddressPostalCode(String) ]; new_type![ /// Region portion of address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AddressRegion(String) ]; new_type![ /// Audience claim value. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] Audience(String) ]; new_type![ /// Authorization code hash. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] AuthorizationCodeHash(String) impl { /// Initialize a new authorization code hash from an [`AuthorizationCode`] and signature /// algorithm. pub fn from_code( code: &AuthorizationCode, alg: &K::SigningAlgorithm, key: &K, ) -> Result where K: JsonWebKey, { key.hash_bytes(code.secret().as_bytes(), alg) .map(|hash| Self::new(BASE64_URL_SAFE_NO_PAD.encode(&hash[0..hash.len() / 2]))) .map_err(SigningError::UnsupportedAlg) } } ]; new_type![ /// OpenID Connect client name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] ClientName(String) ]; new_url_type![ /// Client configuration endpoint URL. ClientConfigUrl ]; new_url_type![ /// Client homepage URL. ClientUrl ]; new_type![ /// Client contact e-mail address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] ClientContactEmail(String) ]; new_url_type![ /// URL for the [OpenID Connect RP-Initiated Logout 1.0]( /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html) end session endpoint. EndSessionUrl ]; new_type![ /// End user's birthday, represented as an /// [ISO 8601:2004](https://www.iso.org/standard/40874.html) `YYYY-MM-DD` format. /// /// The year MAY be `0000`, indicating that it is omitted. To represent only the year, `YYYY` /// format is allowed. Note that depending on the underlying platform's date related function, /// providing just year can result in varying month and day, so the implementers need to take /// this factor into account to correctly process the dates. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserBirthday(String) ]; new_type![ /// End user's e-mail address. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserEmail(String) ]; new_type![ /// End user's family name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserFamilyName(String) ]; new_type![ /// End user's given name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserGivenName(String) ]; new_type![ /// End user's middle name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserMiddleName(String) ]; new_type![ /// End user's name. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserName(String) ]; new_type![ /// End user's nickname. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserNickname(String) ]; new_type![ /// End user's phone number. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserPhoneNumber(String) ]; new_type![ /// URL of end user's profile picture. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserPictureUrl(String) ]; new_type![ /// URL of end user's profile page. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserProfileUrl(String) ]; new_type![ /// End user's time zone as a string from the /// [time zone database](https://www.iana.org/time-zones). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserTimezone(String) ]; new_type![ /// URL of end user's website. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserWebsiteUrl(String) ]; new_type![ /// End user's username. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] EndUserUsername(String) ]; new_type![ /// Full mailing address, formatted for display or use on a mailing label. /// /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented /// either as a carriage return/line feed pair (`"\r\n"`) or as a single line feed character /// (`"\n"`). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] FormattedAddress(String) ]; new_url_type![ /// URI using the `https` scheme that a third party can use to initiate a login by the Relying /// Party. InitiateLoginUrl ]; new_url_type![ /// URL using the `https` scheme with no query or fragment component that the OP asserts as its /// Issuer Identifier. IssuerUrl impl { /// Parse a string as a URL, with this URL as the base URL. /// /// See [`Url::parse`]. pub fn join(&self, suffix: &str) -> Result { if let Some('/') = self.1.chars().next_back() { Url::parse(&(self.1.clone() + suffix)) } else { Url::parse(&(self.1.clone() + "/" + suffix)) } } } ]; new_secret_type![ /// Hint about the login identifier the End-User might use to log in. /// /// The use of this parameter is left to the OpenID Connect Provider's discretion. #[derive(Clone, Deserialize, Serialize)] LoginHint(String) ]; new_secret_type![ /// Hint about the logout identifier the End-User might use to log out. /// /// The use of this parameter is left to the OpenID Connect Provider's discretion. #[derive(Clone, Deserialize, Serialize)] LogoutHint(String) ]; new_url_type![ /// URL that references a logo for the Client application. LogoUrl ]; new_secret_type![ /// String value used to associate a client session with an ID Token, and to mitigate replay /// attacks. #[derive(Clone, Deserialize, Serialize)] Nonce(String) impl { /// Generate a new random, base64-encoded 128-bit nonce. pub fn new_random() -> Self { Nonce::new_random_len(16) } /// Generate a new random, base64-encoded nonce of the specified length. /// /// # Arguments /// /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. pub fn new_random_len(num_bytes: u32) -> Self { let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); Nonce::new(BASE64_URL_SAFE_NO_PAD.encode(random_bytes)) } } ]; new_url_type![ /// URL providing the OpenID Connect Provider's data usage policies for client applications. OpPolicyUrl ]; new_url_type![ /// URL providing the OpenID Connect Provider's Terms of Service. OpTosUrl ]; new_url_type![ /// URL providing a client application's data usage policy. PolicyUrl ]; new_url_type![ /// The post logout redirect URL, which should be passed to the end session endpoint /// of providers implementing [OpenID Connect RP-Initiated Logout 1.0]( /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html). PostLogoutRedirectUrl ]; new_secret_type![ /// Access token used by a client application to access the Client Registration endpoint. #[derive(Clone, Deserialize, Serialize)] RegistrationAccessToken(String) ]; new_url_type![ /// URL of the Client Registration endpoint. RegistrationUrl ]; new_url_type![ /// URL used to pass request parameters as JWTs by reference. RequestUrl ]; /// Informs the Authorization Server of the desired authorization processing flow, including what /// parameters are returned from the endpoints used. /// /// See [OAuth 2.0 Multiple Response Type Encoding Practices]( /// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes) /// for further details. #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct ResponseTypes( #[serde( deserialize_with = "deserialize_space_delimited_vec", serialize_with = "crate::helpers::serialize_space_delimited_vec" )] Vec, ); impl ResponseTypes { /// Create a new [`ResponseTypes`] to wrap the given [`Vec`]. pub fn new(s: Vec) -> Self { ResponseTypes::(s) } } impl Deref for ResponseTypes { type Target = Vec; fn deref(&self) -> &Vec { &self.0 } } new_url_type![ /// URL for retrieving redirect URIs that should receive identical pairwise subject identifiers. SectorIdentifierUrl ]; new_url_type![ /// URL for developer documentation for an OpenID Connect Provider. ServiceDocUrl ]; new_type![ /// A user's street address. /// /// Full street address component, which MAY include house number, street name, Post Office Box, /// and multi-line extended street address information. This field MAY contain multiple lines, /// separated by newlines. Newlines can be represented either as a carriage return/line feed /// pair (`\r\n`) or as a single line feed character (`\n`). #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] StreetAddress(String) ]; new_type![ /// Locally unique and never reassigned identifier within the Issuer for the End-User, which is /// intended to be consumed by the client application. #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] SubjectIdentifier(String) ]; new_url_type![ /// URL for the relying party's Terms of Service. ToSUrl ]; ================================================ FILE: src/types/tests.rs ================================================ use crate::IssuerUrl; #[test] fn test_issuer_url_append() { assert_eq!( "http://example.com/.well-known/openid-configuration", IssuerUrl::new("http://example.com".to_string()) .unwrap() .join(".well-known/openid-configuration") .unwrap() .to_string() ); assert_eq!( "http://example.com/.well-known/openid-configuration", IssuerUrl::new("http://example.com/".to_string()) .unwrap() .join(".well-known/openid-configuration") .unwrap() .to_string() ); assert_eq!( "http://example.com/x/.well-known/openid-configuration", IssuerUrl::new("http://example.com/x".to_string()) .unwrap() .join(".well-known/openid-configuration") .unwrap() .to_string() ); assert_eq!( "http://example.com/x/.well-known/openid-configuration", IssuerUrl::new("http://example.com/x/".to_string()) .unwrap() .join(".well-known/openid-configuration") .unwrap() .to_string() ); } #[test] fn test_url_serialize() { let issuer_url = IssuerUrl::new("http://example.com/.well-known/openid-configuration".to_string()).unwrap(); let serialized_url = serde_json::to_string(&issuer_url).unwrap(); assert_eq!( "\"http://example.com/.well-known/openid-configuration\"", serialized_url ); let deserialized_url = serde_json::from_str(&serialized_url).unwrap(); assert_eq!(issuer_url, deserialized_url); assert_eq!( serde_json::to_string(&IssuerUrl::new("http://example.com".to_string()).unwrap()).unwrap(), "\"http://example.com\"", ); } #[cfg(feature = "accept-string-booleans")] #[test] fn test_string_bool_parse() { use crate::helpers::Boolean; fn test_case(input: &str, expect: bool) { let value: Boolean = serde_json::from_str(input).unwrap(); assert_eq!(value.0, expect); } test_case("true", true); test_case("false", false); test_case("\"true\"", true); test_case("\"false\"", false); assert!(serde_json::from_str::("\"maybe\"").is_err()); } ================================================ FILE: src/user_info.rs ================================================ use crate::helpers::{deserialize_string_or_vec_opt, FilteredFlatten}; use crate::http_utils::{auth_bearer, content_type_has_essence, MIME_TYPE_JSON, MIME_TYPE_JWT}; use crate::jwt::{JsonWebTokenError, JsonWebTokenJsonPayloadSerde}; use crate::verification::UserInfoVerifier; use crate::{ AccessToken, AdditionalClaims, AddressClaim, AsyncHttpClient, Audience, AudiencesClaim, AuthDisplay, AuthPrompt, ClaimsVerificationError, Client, EndUserBirthday, EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, EndUserWebsiteUrl, EndpointState, ErrorResponse, GenderClaim, HttpRequest, HttpResponse, IssuerClaim, IssuerUrl, JsonWebKey, JsonWebToken, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, LocalizedClaim, PrivateSigningKey, RevocableToken, SignatureVerificationError, StandardClaims, SubjectIdentifier, SyncHttpClient, TokenIntrospectionResponse, TokenResponse, }; use chrono::{DateTime, Utc}; use http::header::{HeaderValue, ACCEPT, CONTENT_TYPE}; use http::method::Method; use http::status::StatusCode; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use thiserror::Error; use std::future::Future; use std::str; impl< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > Client< AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE, HasAuthUrl, HasDeviceAuthUrl, HasIntrospectionUrl, HasRevocationUrl, HasTokenUrl, HasUserInfoUrl, > where AC: AdditionalClaims, AD: AuthDisplay, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, P: AuthPrompt, TE: ErrorResponse + 'static, TR: TokenResponse, TIR: TokenIntrospectionResponse, RT: RevocableToken, TRE: ErrorResponse + 'static, HasAuthUrl: EndpointState, HasDeviceAuthUrl: EndpointState, HasIntrospectionUrl: EndpointState, HasRevocationUrl: EndpointState, HasTokenUrl: EndpointState, HasUserInfoUrl: EndpointState, { pub(crate) fn user_info_impl<'a>( &'a self, userinfo_endpoint: &'a UserInfoUrl, access_token: AccessToken, expected_subject: Option, ) -> UserInfoRequest<'a, JE, K> { UserInfoRequest { url: userinfo_endpoint, access_token, require_signed_response: false, response_type: UserInfoResponseType::Json, signed_response_verifier: UserInfoVerifier::new( self.client_id.clone(), self.issuer.clone(), self.jwks.clone(), expected_subject, ), } } } /// User info request. pub struct UserInfoRequest<'a, JE, K> where JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, { pub(crate) url: &'a UserInfoUrl, pub(crate) access_token: AccessToken, pub(crate) require_signed_response: bool, pub(crate) signed_response_verifier: UserInfoVerifier<'static, JE, K>, pub(crate) response_type: UserInfoResponseType, } impl<'a, JE, K> UserInfoRequest<'a, JE, K> where JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, { /// Submits this request to the associated user info endpoint using the specified synchronous /// HTTP client. pub fn request( self, http_client: &C, ) -> Result, UserInfoError<::Error>> where AC: AdditionalClaims, GC: GenderClaim, C: SyncHttpClient, { http_client .call( self.prepare_request().map_err(|err| { UserInfoError::Other(format!("failed to prepare request: {err}")) })?, ) .map_err(UserInfoError::Request) .and_then(|http_response| self.user_info_response(http_response)) } /// Submits this request to the associated user info endpoint using the specified asynchronous /// HTTP client. pub fn request_async<'c, AC, C, GC>( self, http_client: &'c C, ) -> impl Future< Output = Result, UserInfoError<>::Error>>, > + 'c where Self: 'c, AC: AdditionalClaims, C: AsyncHttpClient<'c>, GC: GenderClaim, { Box::pin(async move { let http_response = http_client .call(self.prepare_request().map_err(|err| { UserInfoError::Other(format!("failed to prepare request: {err}")) })?) .await .map_err(UserInfoError::Request)?; self.user_info_response(http_response) }) } fn prepare_request(&self) -> Result { let (auth_header, auth_value) = auth_bearer(&self.access_token); let accept_value = match self.response_type { UserInfoResponseType::Jwt => MIME_TYPE_JWT, _ => MIME_TYPE_JSON, }; http::Request::builder() .uri(self.url.to_string()) .method(Method::GET) .header(ACCEPT, HeaderValue::from_static(accept_value)) .header(auth_header, auth_value) .body(Vec::new()) } fn user_info_response( self, http_response: HttpResponse, ) -> Result, UserInfoError> where AC: AdditionalClaims, GC: GenderClaim, RE: std::error::Error + 'static, { if http_response.status() != StatusCode::OK { return Err(UserInfoError::Response( http_response.status(), http_response.body().to_owned(), "unexpected HTTP status code".to_string(), )); } match http_response .headers() .get(CONTENT_TYPE) .map(ToOwned::to_owned) .unwrap_or_else(|| HeaderValue::from_static(MIME_TYPE_JSON)) { ref content_type if content_type_has_essence(content_type, MIME_TYPE_JSON) => { if self.require_signed_response { return Err(UserInfoError::ClaimsVerification( ClaimsVerificationError::SignatureVerification( SignatureVerificationError::NoSignature, ), )); } UserInfoClaims::from_json( http_response.body(), self.signed_response_verifier.expected_subject(), ) } ref content_type if content_type_has_essence(content_type, MIME_TYPE_JWT) => { let jwt_str = String::from_utf8(http_response.body().to_owned()).map_err(|_| { UserInfoError::Other("response body has invalid UTF-8 encoding".to_string()) })?; serde_path_to_error::deserialize::< _, UserInfoJsonWebToken, >(serde_json::Value::String(jwt_str)) .map_err(UserInfoError::Parse)? .claims(&self.signed_response_verifier) .map_err(UserInfoError::ClaimsVerification) } ref content_type => Err(UserInfoError::Response( http_response.status(), http_response.body().to_owned(), format!("unexpected response Content-Type: `{:?}`", content_type), )), } } /// Specifies whether to require the user info response to be a signed JSON Web Token (JWT). pub fn require_signed_response(mut self, require_signed_response: bool) -> Self { self.require_signed_response = require_signed_response; self } /// Specifies whether to require the issuer of the signed JWT response to match the expected /// issuer URL for this provider. /// /// This option has no effect on unsigned JSON responses. pub fn require_issuer_match(mut self, iss_required: bool) -> Self { self.signed_response_verifier = self .signed_response_verifier .require_issuer_match(iss_required); self } /// Specifies whether to require the audience of the signed JWT response to match the expected /// audience (client ID). /// /// This option has no effect on unsigned JSON responses. pub fn require_audience_match(mut self, aud_required: bool) -> Self { self.signed_response_verifier = self .signed_response_verifier .require_audience_match(aud_required); self } /// Specifies the expected response type by setting the `Accept` header. Note that the server can ignore this header. pub fn set_response_type(mut self, response_type: UserInfoResponseType) -> Self { self.response_type = response_type; self } } /// User info claims. #[derive(Clone, Debug, Serialize)] pub struct UserInfoClaims(UserInfoClaimsImpl); impl UserInfoClaims where AC: AdditionalClaims, GC: GenderClaim, { /// Initializes user info claims. pub fn new(standard_claims: StandardClaims, additional_claims: AC) -> Self { Self(UserInfoClaimsImpl { issuer: None, audiences: None, standard_claims, additional_claims: additional_claims.into(), }) } /// Initializes user info claims from the provided raw JSON response. /// /// If an `expected_subject` is provided, this function verifies that the user info claims /// contain the expected subject and returns an error otherwise. pub fn from_json( user_info_json: &[u8], expected_subject: Option<&SubjectIdentifier>, ) -> Result> where RE: std::error::Error + 'static, { let user_info = serde_path_to_error::deserialize::<_, UserInfoClaimsImpl>( &mut serde_json::Deserializer::from_slice(user_info_json), ) .map_err(UserInfoError::Parse)?; // This is the only verification we need to do for JSON-based user info claims, so don't // bother with the complexity of a separate verifier object. if expected_subject .iter() .all(|expected_subject| user_info.standard_claims.sub == **expected_subject) { Ok(Self(user_info)) } else { Err(UserInfoError::ClaimsVerification( ClaimsVerificationError::InvalidSubject(format!( "expected `{}` (found `{}`)", // This can only happen when expected_subject is not None. expected_subject.unwrap().as_str(), user_info.standard_claims.sub.as_str(), )), )) } } field_getters_setters![ pub self [self.0] ["claim"] { set_issuer -> issuer[Option], set_audiences -> audiences[Option>] ["aud"], } ]; /// Returns the `sub` claim. pub fn subject(&self) -> &SubjectIdentifier { &self.0.standard_claims.sub } /// Sets the `sub` claim. pub fn set_subject(&mut self, subject: SubjectIdentifier) { self.0.standard_claims.sub = subject } field_getters_setters![ pub self [self.0.standard_claims] ["claim"] { set_name -> name[Option>], set_given_name -> given_name[Option>], set_family_name -> family_name[Option>], set_middle_name -> middle_name[Option>], set_nickname -> nickname[Option>], set_preferred_username -> preferred_username[Option], set_profile -> profile[Option>], set_picture -> picture[Option>], set_website -> website[Option>], set_email -> email[Option], set_email_verified -> email_verified[Option], set_gender -> gender[Option], set_birthday -> birthday[Option], set_birthdate -> birthdate[Option], set_zoneinfo -> zoneinfo[Option], set_locale -> locale[Option], set_phone_number -> phone_number[Option], set_phone_number_verified -> phone_number_verified[Option], set_address -> address[Option], set_updated_at -> updated_at[Option>], } ]; /// Returns the standard claims as a `StandardClaims` object. pub fn standard_claims(&self) -> &StandardClaims { &self.0.standard_claims } /// Returns additional user info claims. pub fn additional_claims(&self) -> &AC { self.0.additional_claims.as_ref() } /// Returns mutable additional user info claims. pub fn additional_claims_mut(&mut self) -> &mut AC { self.0.additional_claims.as_mut() } } #[skip_serializing_none] #[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct UserInfoClaimsImpl where AC: AdditionalClaims, GC: GenderClaim, { #[serde(rename = "iss")] pub issuer: Option, // We always serialize as an array, which is valid according to the spec. #[serde( default, rename = "aud", deserialize_with = "deserialize_string_or_vec_opt" )] pub audiences: Option>, #[serde(bound = "GC: GenderClaim", flatten)] pub standard_claims: StandardClaims, #[serde(bound = "AC: AdditionalClaims", flatten)] pub additional_claims: FilteredFlatten, AC>, } impl AudiencesClaim for UserInfoClaimsImpl where AC: AdditionalClaims, GC: GenderClaim, { fn audiences(&self) -> Option<&Vec> { self.audiences.as_ref() } } impl<'a, AC, GC> AudiencesClaim for &'a UserInfoClaimsImpl where AC: AdditionalClaims, GC: GenderClaim, { fn audiences(&self) -> Option<&Vec> { self.audiences.as_ref() } } impl IssuerClaim for UserInfoClaimsImpl where AC: AdditionalClaims, GC: GenderClaim, { fn issuer(&self) -> Option<&IssuerUrl> { self.issuer.as_ref() } } impl<'a, AC, GC> IssuerClaim for &'a UserInfoClaimsImpl where AC: AdditionalClaims, GC: GenderClaim, { fn issuer(&self) -> Option<&IssuerUrl> { self.issuer.as_ref() } } /// JSON Web Token (JWT) containing user info claims. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct UserInfoJsonWebToken< AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, >( #[serde(bound = "AC: AdditionalClaims")] JsonWebToken, JsonWebTokenJsonPayloadSerde>, ); impl UserInfoJsonWebToken where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm, JS: JwsSigningAlgorithm, { /// Initializes a new signed JWT containing the specified claims, signed with the specified key /// and signing algorithm. pub fn new( claims: UserInfoClaims, signing_key: &S, alg: JS, ) -> Result where S: PrivateSigningKey, ::VerificationKey: JsonWebKey, { Ok(Self(JsonWebToken::new(claims.0, signing_key, &alg)?)) } /// Verifies and returns the user info claims. pub fn claims( self, verifier: &UserInfoVerifier, ) -> Result, ClaimsVerificationError> where K: JsonWebKey, { Ok(UserInfoClaims(verifier.verified_claims(self.0)?)) } } new_url_type![ /// URL for a provider's user info endpoint. UserInfoUrl ]; /// Indicates via the `Accept` header the body response type the server should use to return the user info. Note that the server can ignore this header. /// /// Defaults to Json. #[derive(Clone, Debug)] #[non_exhaustive] pub enum UserInfoResponseType { /// Sets the `Accept` header to `application/json`. Json, /// Sets the `Accept` header to `application/jwt`. Jwt, } /// Error retrieving user info. #[derive(Debug, Error)] #[non_exhaustive] pub enum UserInfoError where RE: std::error::Error + 'static, { /// Failed to verify user info claims. #[error("Failed to verify claims")] ClaimsVerification(#[source] ClaimsVerificationError), /// Failed to parse server response. #[error("Failed to parse server response")] Parse(#[source] serde_path_to_error::Error), /// An error occurred while sending the request or receiving the response (e.g., network /// connectivity failed). #[error("Request failed")] Request(#[source] RE), /// Server returned an invalid response. #[error("Server returned invalid response: {2}")] Response(StatusCode, Vec, String), /// An unexpected error occurred. #[error("Other error: {0}")] Other(String), } #[cfg(test)] mod tests { use crate::core::CoreGenderClaim; use crate::{AdditionalClaims, UserInfoClaims}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] struct TestClaims { pub tfa_method: String, } impl AdditionalClaims for TestClaims {} #[test] fn test_additional_claims() { let claims = UserInfoClaims::::from_json::( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\"], \"tfa_method\": \"u2f\" }" .as_bytes(), None, ) .expect("failed to deserialize"); assert_eq!(claims.additional_claims().tfa_method, "u2f"); assert_eq!( serde_json::to_string(&claims).expect("failed to serialize"), "{\ \"iss\":\"https://server.example.com\",\ \"aud\":[\"s6BhdRkqt3\"],\ \"sub\":\"24400320\",\ \"tfa_method\":\"u2f\"\ }", ); UserInfoClaims::::from_json::( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\"] }" .as_bytes(), None, ) .expect_err("missing claim should fail to deserialize"); } #[derive(Debug, Deserialize, Serialize)] struct AllOtherClaims(HashMap); impl AdditionalClaims for AllOtherClaims {} #[test] fn test_catch_all_additional_claims() { let claims = UserInfoClaims::::from_json::( "{ \"iss\": \"https://server.example.com\", \"sub\": \"24400320\", \"aud\": [\"s6BhdRkqt3\"], \"tfa_method\": \"u2f\", \"updated_at\": 1000 }" .as_bytes(), None, ) .expect("failed to deserialize"); assert_eq!(claims.additional_claims().0.len(), 1); assert_eq!(claims.additional_claims().0["tfa_method"], "u2f"); } } ================================================ FILE: src/verification/mod.rs ================================================ use crate::jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde, NormalizedJsonWebTokenType}; use crate::user_info::UserInfoClaimsImpl; use crate::{ AdditionalClaims, Audience, AuthenticationContextClass, ClientId, ClientSecret, GenderClaim, IdTokenClaims, IssuerUrl, JsonWebKey, JsonWebKeyId, JsonWebKeySet, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenHeader, JsonWebTokenType, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, Nonce, SubjectIdentifier, }; use chrono::{DateTime, Utc}; use serde::de::DeserializeOwned; use serde::Serialize; use sha2::{Digest, Sha256}; use thiserror::Error; use std::collections::HashSet; use std::fmt::Debug; use std::marker::PhantomData; use std::ops::Deref; use std::sync::Arc; #[cfg(test)] mod tests; pub(crate) trait AudiencesClaim { fn audiences(&self) -> Option<&Vec>; } pub(crate) trait IssuerClaim { fn issuer(&self) -> Option<&IssuerUrl>; } /// Error verifying claims. #[derive(Clone, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub enum ClaimsVerificationError { /// Claims have expired. #[error("Expired: {0}")] Expired(String), /// Audience claim is invalid. #[error("Invalid audiences: {0}")] InvalidAudience(String), /// Authorization context class reference (`acr`) claim is invalid. #[error("Invalid authorization context class reference: {0}")] InvalidAuthContext(String), /// User authenticated too long ago. #[error("Invalid authentication time: {0}")] InvalidAuthTime(String), /// Issuer claim is invalid. #[error("Invalid issuer: {0}")] InvalidIssuer(String), /// Nonce is invalid. #[error("Invalid nonce: {0}")] InvalidNonce(String), /// Subject claim is invalid. #[error("Invalid subject: {0}")] InvalidSubject(String), /// An unexpected error occurred. #[error("{0}")] Other(String), /// Failed to verify the claims signature. #[error("Signature verification failed")] SignatureVerification(#[source] SignatureVerificationError), /// Unsupported argument or value. #[error("Unsupported: {0}")] Unsupported(String), } /// Error verifying claims signature. #[derive(Clone, Debug, Error, PartialEq, Eq)] #[non_exhaustive] pub enum SignatureVerificationError { /// More than one key matches the supplied key constraints (e.g., key ID). #[error("Ambiguous key identification: {0}")] AmbiguousKeyId(String), /// Invalid signature for the supplied claims and signing key. #[error("Crypto error: {0}")] CryptoError(String), /// The supplied signature algorithm is disallowed by the verifier. #[error("Disallowed signature algorithm: {0}")] DisallowedAlg(String), /// The supplied key cannot be used in this context. This may occur if the key type does not /// match the signature type (e.g., an RSA key used to validate an HMAC) or the JWK usage /// disallows signatures. #[error("Invalid cryptographic key: {0}")] InvalidKey(String), /// The signing key needed for verifying the /// [JSON Web Token](https://tools.ietf.org/html/rfc7519)'s signature/MAC could not be found. /// This error can occur if the key ID (`kid`) specified in the JWT's /// [JOSE header](https://tools.ietf.org/html/rfc7519#section-5) does not match the ID of any /// key in the OpenID Connect provider's JSON Web Key Set (JWKS), typically retrieved from /// the provider's [JWKS document]( /// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). To support /// [rotation of asymmetric signing keys]( /// http://openid.net/specs/openid-connect-core-1_0.html#RotateSigKeys), client applications /// should consider refreshing the JWKS document (via /// [`JsonWebKeySet::fetch`][crate::JsonWebKeySet::fetch]). /// /// This error can also occur if the identified /// [JSON Web Key](https://tools.ietf.org/html/rfc7517) is of the wrong type (e.g., an RSA key /// when the JOSE header specifies an ECDSA algorithm) or does not support signing. #[error("No matching key found")] NoMatchingKey, /// No signature present but claims must be signed. #[error("No signature found")] NoSignature, /// Unsupported signature algorithm. #[error("Unsupported signature algorithm: {0}")] UnsupportedAlg(String), /// An unexpected error occurred. #[error("Other error: {0}")] Other(String), } // This struct is intentionally private. #[derive(Clone)] pub(crate) struct JwtClaimsVerifier<'a, K> where K: JsonWebKey, { allowed_jose_types: Option>, allowed_algs: Option>, aud_match_required: bool, client_id: ClientId, client_secret: Option, iss_required: bool, issuer: IssuerUrl, is_signature_check_enabled: bool, other_aud_verifier_fn: Arc bool + 'a + Send + Sync>, signature_keys: JsonWebKeySet, } impl<'a, K> JwtClaimsVerifier<'a, K> where K: JsonWebKey, { pub fn new(client_id: ClientId, issuer: IssuerUrl, signature_keys: JsonWebKeySet) -> Self { JwtClaimsVerifier { allowed_algs: Some( [K::SigningAlgorithm::rsa_sha_256()] .iter() .cloned() .collect(), ), allowed_jose_types: Some(HashSet::from([ JsonWebTokenType::new("application/jwt".to_string()) .normalize() .expect("application/jwt should be a valid JWT type"), // used by many IdP, but not standardized JsonWebTokenType::new("application/jose".to_string()) .normalize() .expect("application/jose should be a valid JWT type"), // standard as defined in https://tools.ietf.org/html/rfc7515#section-4.1.9 // we do not support JOSE+JSON, so we omit this here in the default configuration ])), aud_match_required: true, client_id, client_secret: None, iss_required: true, issuer, is_signature_check_enabled: true, // Secure default: reject all other audiences as untrusted, since any other audience // can potentially impersonate the user when by sending its copy of these claims // to this relying party. other_aud_verifier_fn: Arc::new(|_| false), signature_keys, } } pub fn require_audience_match(mut self, aud_required: bool) -> Self { self.aud_match_required = aud_required; self } pub fn require_issuer_match(mut self, iss_required: bool) -> Self { self.iss_required = iss_required; self } pub fn require_signature_check(mut self, sig_required: bool) -> Self { self.is_signature_check_enabled = sig_required; self } pub fn set_allowed_algs(mut self, algs: I) -> Self where I: IntoIterator, { self.allowed_algs = Some(algs.into_iter().collect()); self } pub fn allow_any_alg(mut self) -> Self { self.allowed_algs = None; self } /// Allows setting specific JOSE types. The verifier will check against them during verification. /// /// See [RFC 7515 section 4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9) for more details. pub fn set_allowed_jose_types(mut self, types: I) -> Self where I: IntoIterator, { self.allowed_jose_types = Some(types.into_iter().collect()); self } pub fn allow_all_jose_types(mut self) -> Self { self.allowed_jose_types = None; self } pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self { self.client_secret = Some(client_secret); self } pub fn set_other_audience_verifier_fn(mut self, other_aud_verifier_fn: T) -> Self where T: Fn(&Audience) -> bool + 'a + Send + Sync, { self.other_aud_verifier_fn = Arc::new(other_aud_verifier_fn); self } fn validate_jose_header( &self, jose_header: &JsonWebTokenHeader, ) -> Result<(), ClaimsVerificationError> where JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, { // The 'typ' header field must either be omitted or have the canonicalized value JWT. // see https://tools.ietf.org/html/rfc7519#section-5.1 if let Some(ref jwt_type) = jose_header.typ { if let Some(allowed_jose_types) = &self.allowed_jose_types { // Check according to https://tools.ietf.org/html/rfc7515#section-4.1.9 // See https://tools.ietf.org/html/rfc2045#section-5.1 for the full Content-Type Header Field spec. // // For sake of simplicity, we do not support matching on application types with parameters like // application/example;part="1/2". If you know your parameters exactly, just set the whole Content Type manually. let valid_jwt_type = if let Ok(normalized_jwt_type) = jwt_type.normalize() { allowed_jose_types.contains(&normalized_jwt_type) } else { false }; if !valid_jwt_type { return Err(ClaimsVerificationError::Unsupported(format!( "unexpected or unsupported JWT type `{}`", **jwt_type ))); } } } // The 'cty' header field must be omitted, since it's only used for JWTs that contain // content types other than JSON-encoded claims. This may include nested JWTs, such as if // JWE encryption is used. This is currently unsupported. if let Some(ref content_type) = jose_header.cty { if content_type.to_uppercase() == "JWT" { return Err(ClaimsVerificationError::Unsupported( "nested JWT's are not currently supported".to_string(), )); } else { return Err(ClaimsVerificationError::Unsupported(format!( "unexpected or unsupported JWT content type `{}`", **content_type ))); } } // If 'crit' fields are specified, we must reject any we do not understand. Since this // implementation doesn't understand any of them, unconditionally reject the JWT. Note that // the spec prohibits this field from containing any of the standard headers or being empty. if jose_header.crit.is_some() { // https://tools.ietf.org/html/rfc7515#appendix-E return Err(ClaimsVerificationError::Unsupported( "critical JWT header fields are unsupported".to_string(), )); } Ok(()) } pub fn verified_claims(&self, jwt: A) -> Result where A: JsonWebTokenAccess, C: AudiencesClaim + Debug + DeserializeOwned + IssuerClaim + Serialize, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, T: AudiencesClaim + IssuerClaim, { { let jose_header = jwt.unverified_header(); self.validate_jose_header(jose_header)?; // The code below roughly follows the validation steps described in // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // 1. If the ID Token is encrypted, decrypt it using the keys and algorithms that the Client // specified during Registration that the OP was to use to encrypt the ID Token. If // encryption was negotiated with the OP at Registration time and the ID Token is not // encrypted, the RP SHOULD reject it. if let JsonWebTokenAlgorithm::Encryption(ref encryption_alg) = jose_header.alg { return Err(ClaimsVerificationError::Unsupported(format!( "JWE encryption is not currently supported (found algorithm `{}`)", serde_plain::to_string(encryption_alg).unwrap_or_else(|err| panic!( "encryption alg {:?} failed to serialize to a string: {}", encryption_alg, err )), ))); } } // TODO: Add encryption (JWE) support { // 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during // Discovery) MUST exactly match the value of the iss (issuer) Claim. let unverified_claims = jwt.unverified_payload_ref(); if self.iss_required { if let Some(issuer) = unverified_claims.issuer() { if *issuer != self.issuer { return Err(ClaimsVerificationError::InvalidIssuer(format!( "expected `{}` (found `{}`)", *self.issuer, **issuer ))); } } else { return Err(ClaimsVerificationError::InvalidIssuer( "missing issuer claim".to_string(), )); } } // 3. The Client MUST validate that the aud (audience) Claim contains its client_id value // registered at the Issuer identified by the iss (issuer) Claim as an audience. The aud // (audience) Claim MAY contain an array with more than one element. The ID Token MUST be // rejected if the ID Token does not list the Client as a valid audience, or if it // contains additional audiences not trusted by the Client. if self.aud_match_required { if let Some(audiences) = unverified_claims.audiences() { if !audiences .iter() .any(|aud| (**aud).deref() == self.client_id.deref()) { return Err(ClaimsVerificationError::InvalidAudience(format!( "must contain `{}` (found audiences: {})", *self.client_id, audiences .iter() .map(|aud| format!("`{}`", Deref::deref(aud))) .collect::>() .join(", ") ))); } else if audiences.len() > 1 { audiences .iter() .filter(|aud| (**aud).deref() != self.client_id.deref()) .find(|aud| !(self.other_aud_verifier_fn)(aud)) .map(|aud| { Err(ClaimsVerificationError::InvalidAudience(format!( "`{}` is not a trusted audience", **aud, ))) }) .unwrap_or(Ok(()))?; } } else { return Err(ClaimsVerificationError::InvalidAudience( "missing audiences claim".to_string(), )); } } } // Steps 4--5 (azp claim validation) are specific to the ID token. // 6. If the ID Token is received via direct communication between the Client and the Token // Endpoint (which it is in this flow), the TLS server validation MAY be used to validate // the issuer in place of checking the token signature. The Client MUST validate the // signature of all other ID Tokens according to JWS [JWS] using the algorithm specified // in the JWT alg Header Parameter. The Client MUST use the keys provided by the Issuer. if !self.is_signature_check_enabled { return Ok(jwt.unverified_payload()); } // Borrow the header again. We had to drop the reference above to allow for the // early exit calling jwt.unverified_claims(), which takes ownership of the JWT. let signature_alg = jwt .signing_alg() .map_err(ClaimsVerificationError::SignatureVerification)? .to_owned(); // 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client // in the id_token_signed_response_alg parameter during Registration. if let Some(ref allowed_algs) = self.allowed_algs { if !allowed_algs.contains(&signature_alg) { return Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::DisallowedAlg(format!( "algorithm `{}` is not one of: {}", serde_plain::to_string(&signature_alg).unwrap_or_else(|err| panic!( "signature alg {:?} failed to serialize to a string: {}", signature_alg, err, )), allowed_algs .iter() .map( |alg| serde_plain::to_string(alg).unwrap_or_else(|err| panic!( "signature alg {:?} failed to serialize to a string: {}", alg, err, )) ) .collect::>() .join(", "), )), )); } } // NB: We must *not* trust the 'kid' (key ID) or 'alg' (algorithm) fields present in the // JOSE header, as an attacker could manipulate these while forging the JWT. The code // below must be secure regardless of how these fields are manipulated. if signature_alg.uses_shared_secret() { // 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, // HS384, or HS512, the octets of the UTF-8 representation of the client_secret // corresponding to the client_id contained in the aud (audience) Claim are used // as the key to validate the signature. For MAC based algorithms, the behavior // is unspecified if the aud is multi-valued or if an azp value is present that // is different than the aud value. if let Some(ref client_secret) = self.client_secret { let key = K::new_symmetric(client_secret.secret().clone().into_bytes()); return jwt .payload(&signature_alg, &key) .map_err(ClaimsVerificationError::SignatureVerification); } else { // The client secret isn't confidential for public clients, so anyone can forge a // JWT with a valid signature. return Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::DisallowedAlg( "symmetric signatures are disallowed for public clients".to_string(), ), )); } } // Section 10.1 of OpenID Connect Core 1.0 states that the JWT must include a key ID // if the JWK set contains more than one public key. let public_key = self .signing_key(jwt.unverified_header().kid.as_ref(), &signature_alg) .map_err(ClaimsVerificationError::SignatureVerification)?; jwt.payload(&signature_alg.clone(), public_key) .map_err(ClaimsVerificationError::SignatureVerification) // Steps 9--13 are specific to the ID token. } pub(crate) fn signing_key<'b>( &'b self, key_id: Option<&JsonWebKeyId>, signature_alg: &K::SigningAlgorithm, ) -> Result<&'b K, SignatureVerificationError> { // See if any key has a matching key ID (if supplied) and compatible type. let public_keys = self.signature_keys.filter_keys(key_id, signature_alg); if public_keys.is_empty() { Err(SignatureVerificationError::NoMatchingKey) } else if public_keys.len() == 1 { Ok(public_keys.first().expect("unreachable")) } else { Err(SignatureVerificationError::AmbiguousKeyId(format!( "JWK set must only contain one eligible public key, but found {} eligible keys: {}", public_keys.len(), public_keys .iter() .map(|key| format!( "{} ({})", key.key_id() .map(|kid| format!("`{}`", **kid)) .unwrap_or_else(|| "null ID".to_string()), serde_plain::to_string(key.key_type()).unwrap_or_else(|err| panic!( "key type {:?} failed to serialize to a string: {}", key.key_type(), err, )) )) .collect::>() .join(", ") ))) } } } /// Trait for verifying ID token nonces. pub trait NonceVerifier { /// Verifies the nonce. /// /// Returns `Ok(())` if the nonce is valid, or a string describing the error otherwise. fn verify(self, nonce: Option<&Nonce>) -> Result<(), String>; } impl NonceVerifier for &Nonce { fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> { if let Some(claims_nonce) = nonce { // Avoid timing side-channel. if Sha256::digest(claims_nonce.secret()) != Sha256::digest(self.secret()) { return Err("nonce mismatch".to_string()); } } else { return Err("missing nonce claim".to_string()); } Ok(()) } } impl NonceVerifier for F where F: FnOnce(Option<&Nonce>) -> Result<(), String>, { fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> { self(nonce) } } /// ID token verifier. #[derive(Clone)] pub struct IdTokenVerifier<'a, K> where K: JsonWebKey, { acr_verifier_fn: Arc) -> Result<(), String> + 'a + Send + Sync>, #[allow(clippy::type_complexity)] auth_time_verifier_fn: Arc>) -> Result<(), String> + 'a + Send + Sync>, iat_verifier_fn: Arc) -> Result<(), String> + 'a + Send + Sync>, pub(crate) jwt_verifier: JwtClaimsVerifier<'a, K>, time_fn: Arc DateTime + 'a + Send + Sync>, } impl<'a, K> IdTokenVerifier<'a, K> where K: JsonWebKey, { fn new(jwt_verifier: JwtClaimsVerifier<'a, K>) -> Self { IdTokenVerifier { // By default, accept authorization context reference (acr claim). acr_verifier_fn: Arc::new(|_| Ok(())), auth_time_verifier_fn: Arc::new(|_| Ok(())), // By default, accept any issued time (iat claim). iat_verifier_fn: Arc::new(|_| Ok(())), jwt_verifier, // By default, use the current system time. time_fn: Arc::new(Utc::now), } } /// Initializes a new verifier for a public client (i.e., one without a client secret). pub fn new_public_client( client_id: ClientId, issuer: IssuerUrl, signature_keys: JsonWebKeySet, ) -> Self { Self::new(JwtClaimsVerifier::new(client_id, issuer, signature_keys)) } /// Initializes a no-op verifier that performs no signature, audience, or issuer verification. /// The token's expiration time is still checked, and the token is otherwise required to conform to the expected format. pub fn new_insecure_without_verification() -> Self { let empty_issuer = IssuerUrl::new("https://0.0.0.0".to_owned()) .expect("Creating empty issuer url mustn't fail"); Self::new_public_client( ClientId::new(String::new()), empty_issuer, JsonWebKeySet::new(vec![]), ) .insecure_disable_signature_check() .require_audience_match(false) .require_issuer_match(false) } /// Initializes a new verifier for a confidential client (i.e., one with a client secret). /// /// A confidential client verifier is required in order to verify ID tokens signed using a /// shared secret algorithm such as `HS256`, `HS384`, or `HS512`. For these algorithms, the /// client secret is the shared secret. pub fn new_confidential_client( client_id: ClientId, client_secret: ClientSecret, issuer: IssuerUrl, signature_keys: JsonWebKeySet, ) -> Self { Self::new( JwtClaimsVerifier::new(client_id, issuer, signature_keys) .set_client_secret(client_secret), ) } /// Specifies which JSON Web Signature algorithms are supported. pub fn set_allowed_algs(mut self, algs: I) -> Self where I: IntoIterator, { self.jwt_verifier = self.jwt_verifier.set_allowed_algs(algs); self } /// Specifies that any signature algorithm is supported. pub fn allow_any_alg(mut self) -> Self { self.jwt_verifier = self.jwt_verifier.allow_any_alg(); self } /// Allows setting specific JOSE types. The verifier will check against them during verification. /// /// See [RFC 7515 section 4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9) for more details. pub fn set_allowed_jose_types(mut self, types: I) -> Self where I: IntoIterator, { self.jwt_verifier = self.jwt_verifier.set_allowed_jose_types(types); self } /// Allow all JSON Web Token Header types. pub fn allow_all_jose_types(mut self) -> Self { self.jwt_verifier = self.jwt_verifier.allow_all_jose_types(); self } /// Specifies a function for verifying the `acr` claim. /// /// The function should return `Ok(())` if the claim is valid, or a string describing the error /// otherwise. pub fn set_auth_context_verifier_fn(mut self, acr_verifier_fn: T) -> Self where T: Fn(Option<&AuthenticationContextClass>) -> Result<(), String> + 'a + Send + Sync, { self.acr_verifier_fn = Arc::new(acr_verifier_fn); self } /// Specifies a function for verifying the `auth_time` claim. /// /// The function should return `Ok(())` if the claim is valid, or a string describing the error /// otherwise. pub fn set_auth_time_verifier_fn(mut self, auth_time_verifier_fn: T) -> Self where T: Fn(Option>) -> Result<(), String> + 'a + Send + Sync, { self.auth_time_verifier_fn = Arc::new(auth_time_verifier_fn); self } /// Enables signature verification. /// /// Signature verification is enabled by default, so this function is only useful if /// [`IdTokenVerifier::insecure_disable_signature_check`] was previously invoked. pub fn enable_signature_check(mut self) -> Self { self.jwt_verifier = self.jwt_verifier.require_signature_check(true); self } /// Disables signature verification. /// /// # Security Warning /// /// Unverified ID tokens may be subject to forgery. See [Section 16.3]( /// https://openid.net/specs/openid-connect-core-1_0.html#TokenManufacture) for more /// information. pub fn insecure_disable_signature_check(mut self) -> Self { self.jwt_verifier = self.jwt_verifier.require_signature_check(false); self } /// Specifies whether the issuer claim must match the expected issuer URL for the provider. pub fn require_issuer_match(mut self, iss_required: bool) -> Self { self.jwt_verifier = self.jwt_verifier.require_issuer_match(iss_required); self } /// Specifies whether the audience claim must match this client's client ID. pub fn require_audience_match(mut self, aud_required: bool) -> Self { self.jwt_verifier = self.jwt_verifier.require_audience_match(aud_required); self } /// Specifies a function for returning the current time. /// /// This function is used for verifying the ID token expiration time. pub fn set_time_fn(mut self, time_fn: T) -> Self where T: Fn() -> DateTime + 'a + Send + Sync, { self.time_fn = Arc::new(time_fn); self } /// Specifies a function for verifying the ID token issue time. /// /// The function should return `Ok(())` if the claim is valid, or a string describing the error /// otherwise. pub fn set_issue_time_verifier_fn(mut self, iat_verifier_fn: T) -> Self where T: Fn(DateTime) -> Result<(), String> + 'a + Send + Sync, { self.iat_verifier_fn = Arc::new(iat_verifier_fn); self } /// Specifies a function for verifying audiences included in the `aud` claim that differ from /// this client's client ID. /// /// The function should return `true` if the audience is trusted, or `false` otherwise. /// /// [Section 3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation) /// states that *"The ID Token MUST be rejected if the ID Token does not list the Client as a /// valid audience, or if it contains additional audiences not trusted by the Client."* pub fn set_other_audience_verifier_fn(mut self, other_aud_verifier_fn: T) -> Self where T: Fn(&Audience) -> bool + 'a + Send + Sync, { self.jwt_verifier = self .jwt_verifier .set_other_audience_verifier_fn(other_aud_verifier_fn); self } pub(crate) fn verified_claims<'b, AC, GC, JE, N>( &self, jwt: &'b JsonWebToken< JE, K::SigningAlgorithm, IdTokenClaims, JsonWebTokenJsonPayloadSerde, >, nonce_verifier: N, ) -> Result<&'b IdTokenClaims, ClaimsVerificationError> where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, N: NonceVerifier, { // The code below roughly follows the validation steps described in // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // Steps 1--3 are handled by the generic JwtClaimsVerifier. let partially_verified_claims = self.jwt_verifier.verified_claims(jwt)?; self.verify_claims(partially_verified_claims, nonce_verifier)?; Ok(partially_verified_claims) } pub(crate) fn verified_claims_owned( &self, jwt: JsonWebToken< JE, K::SigningAlgorithm, IdTokenClaims, JsonWebTokenJsonPayloadSerde, >, nonce_verifier: N, ) -> Result, ClaimsVerificationError> where AC: AdditionalClaims, GC: GenderClaim, JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, N: NonceVerifier, { // The code below roughly follows the validation steps described in // https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation // Steps 1--3 are handled by the generic JwtClaimsVerifier. let partially_verified_claims = self.jwt_verifier.verified_claims(jwt)?; self.verify_claims(&partially_verified_claims, nonce_verifier)?; Ok(partially_verified_claims) } fn verify_claims( &self, partially_verified_claims: &'_ IdTokenClaims, nonce_verifier: N, ) -> Result<(), ClaimsVerificationError> where AC: AdditionalClaims, GC: GenderClaim, N: NonceVerifier, { // 4. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp // Claim is present. // There is significant confusion and contradiction in the OpenID Connect Core spec around // the azp claim. See https://bitbucket.org/openid/connect/issues/973/ for a detailed // discussion. Given the lack of clarity around how this claim should be used, we defer // any verification of it here until a use case becomes apparent. If such a use case does // arise, we most likely want to allow clients to pass in a function for validating the // azp claim rather than introducing logic that affects all clients of this library. // This naive implementation of the spec would almost certainly not be useful in practice: /* let azp_required = partially_verified_claims.audiences().len() > 1; // 5. If an azp (authorized party) Claim is present, the Client SHOULD verify that its // client_id is the Claim Value. if let Some(authorized_party) = partially_verified_claims.authorized_party() { if *authorized_party != self.client_id { return Err(ClaimsVerificationError::InvalidAudience(format!( "authorized party must match client ID `{}` (found `{}`", *self.client_id, **authorized_party ))); } } else if azp_required { return Err(ClaimsVerificationError::InvalidAudience(format!( "missing authorized party claim but multiple audiences found" ))); } */ // Steps 6--8 are handled by the generic JwtClaimsVerifier. // 9. The current time MUST be before the time represented by the exp Claim. let cur_time = (*self.time_fn)(); if cur_time >= partially_verified_claims.expiration() { return Err(ClaimsVerificationError::Expired(format!( "ID token expired at {} (current time is {})", partially_verified_claims.expiration(), cur_time ))); } // 10. The iat Claim can be used to reject tokens that were issued too far away from the // current time, limiting the amount of time that nonces need to be stored to prevent // attacks. The acceptable range is Client specific. (*self.iat_verifier_fn)(partially_verified_claims.issue_time()) .map_err(ClaimsVerificationError::Expired)?; // 11. If a nonce value was sent in the Authentication Request, a nonce Claim MUST be // present and its value checked to verify that it is the same value as the one that was // sent in the Authentication Request. The Client SHOULD check the nonce value for // replay attacks. The precise method for detecting replay attacks is Client specific. nonce_verifier .verify(partially_verified_claims.nonce()) .map_err(ClaimsVerificationError::InvalidNonce)?; // 12. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value // is appropriate. The meaning and processing of acr Claim Values is out of scope for // this specification. (*self.acr_verifier_fn)(partially_verified_claims.auth_context_ref()) .map_err(ClaimsVerificationError::InvalidAuthContext)?; // 13. If the auth_time Claim was requested, either through a specific request for this // Claim or by using the max_age parameter, the Client SHOULD check the auth_time Claim // value and request re-authentication if it determines too much time has elapsed since // the last End-User authentication. (*self.auth_time_verifier_fn)(partially_verified_claims.auth_time()) .map_err(ClaimsVerificationError::InvalidAuthTime)?; Ok(()) } } /// User info verifier. #[derive(Clone)] pub struct UserInfoVerifier<'a, JE, K> where JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, { jwt_verifier: JwtClaimsVerifier<'a, K>, expected_subject: Option, _phantom: PhantomData, } impl<'a, JE, K> UserInfoVerifier<'a, JE, K> where JE: JweContentEncryptionAlgorithm< KeyType = ::KeyType, >, K: JsonWebKey, { /// Instantiates a user info verifier. pub fn new( client_id: ClientId, issuer: IssuerUrl, signature_keys: JsonWebKeySet, expected_subject: Option, ) -> Self { UserInfoVerifier { jwt_verifier: JwtClaimsVerifier::new(client_id, issuer, signature_keys), expected_subject, _phantom: PhantomData, } } pub(crate) fn expected_subject(&self) -> Option<&SubjectIdentifier> { self.expected_subject.as_ref() } /// Specifies whether the issuer claim must match the expected issuer URL for the provider. pub fn require_issuer_match(mut self, iss_required: bool) -> Self { self.jwt_verifier = self.jwt_verifier.require_issuer_match(iss_required); self } /// Specifies whether the audience claim must match this client's client ID. pub fn require_audience_match(mut self, aud_required: bool) -> Self { self.jwt_verifier = self.jwt_verifier.require_audience_match(aud_required); self } pub(crate) fn verified_claims( &self, user_info_jwt: JsonWebToken< JE, K::SigningAlgorithm, UserInfoClaimsImpl, JsonWebTokenJsonPayloadSerde, >, ) -> Result, ClaimsVerificationError> where AC: AdditionalClaims, GC: GenderClaim, { let user_info = self.jwt_verifier.verified_claims(user_info_jwt)?; if self .expected_subject .iter() .all(|expected_subject| user_info.standard_claims.sub == *expected_subject) { Ok(user_info) } else { Err(ClaimsVerificationError::InvalidSubject(format!( "expected `{}` (found `{}`)", // This can only happen when self.expected_subject is not None. self.expected_subject.as_ref().unwrap().as_str(), user_info.standard_claims.sub.as_str() ))) } } } ================================================ FILE: src/verification/tests.rs ================================================ use crate::core::{ CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKey, CoreJsonWebKeySet, CoreJsonWebKeyType, CoreJsonWebKeyUse, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreRsaPrivateSigningKey, CoreUserInfoClaims, CoreUserInfoJsonWebToken, CoreUserInfoVerifier, }; use crate::helpers::{Base64UrlEncodedBytes, Timestamp}; use crate::jwt::tests::{TEST_RSA_PRIV_KEY, TEST_RSA_PUB_KEY}; use crate::jwt::{ JsonWebToken, JsonWebTokenHeader, JsonWebTokenJsonPayloadSerde, JsonWebTokenType, }; use crate::verification::{AudiencesClaim, IssuerClaim, JwtClaimsVerifier}; use crate::{ AccessToken, Audience, AuthenticationContextClass, AuthorizationCode, ClaimsVerificationError, ClientId, ClientSecret, EndUserName, IssuerUrl, JsonWebKeyId, Nonce, SignatureVerificationError, StandardClaims, SubjectIdentifier, UserInfoError, }; use chrono::{TimeZone, Utc}; use serde::{Deserialize, Serialize}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; type CoreJsonWebTokenHeader = JsonWebTokenHeader; type CoreJwtClaimsVerifier<'a> = JwtClaimsVerifier<'a, CoreJsonWebKey>; fn assert_unsupported(result: Result, expected_substr: &str) { match result { Err(ClaimsVerificationError::Unsupported(msg)) => { assert!(msg.contains(expected_substr)) } Err(err) => panic!("unexpected error: {:?}", err), Ok(_) => panic!("validation should fail"), } } #[test] fn test_jose_header() { let client_id = ClientId::new("my_client".to_string()); let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); let verifier = CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![]), ); // Happy path JWT verifier .validate_jose_header( &serde_json::from_str::("{\"alg\":\"RS256\", \"typ\":\"JWT\"}") .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); verifier .validate_jose_header( &serde_json::from_str::("{\"alg\":\"RS256\", \"typ\":\"jwt\"}") .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\", \"typ\":\"application/JWT\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); // Happy path JOSE verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\", \"typ\":\"jose\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\", \"typ\":\"JOSE\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); // Unexpected JWT type. assert_unsupported( verifier.validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"NOT_A_JWT\"}", ) .expect("failed to deserialize"), ), "unsupported JWT type", ); // No typ at all. verifier .validate_jose_header( &serde_json::from_str::("{\"alg\":\"RS256\"}") .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); // Specific JWT type from list. { let custom_verifier = CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![]), ) .set_allowed_jose_types(vec![ JsonWebTokenType::new("application/NOT_A_JWT".to_string()) .normalize() .unwrap(), JsonWebTokenType::new("APPLICATION/AT+jwt".to_string()) .normalize() .unwrap(), JsonWebTokenType::new("X-special-app/jwt;param=some".to_string()) .normalize() .unwrap(), JsonWebTokenType::new("X-special-app/jwt".to_string()) .normalize() .unwrap(), ]); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"NOT_A_JWT\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"application/NOT_A_JWT\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); assert_unsupported( custom_verifier.validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"application/NOT_A_JWT;bla=test\"}", ) .expect("failed to deserialize"), ), "unsupported JWT type", ); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"application/at+jwt\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); assert_unsupported( custom_verifier.validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"NOT_A_JWT_REALLY\"}", ) .expect("failed to deserialize"), ), "unsupported JWT type", ); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"X-special-app/jwt\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"X-special-app/jwt;param=some\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); assert_unsupported( custom_verifier.validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"X-special-app/jwt;param=other\"}", ) .expect("failed to deserialize"), ), "unsupported JWT type", ); } // Allow all JWT types. { let custom_verifier = CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![]), ) .allow_all_jose_types(); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"NOT_A_JWT\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"application/at+jwt\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"application/at+jwt;oidc=cool\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); custom_verifier .validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"typ\":\"NOT_A_JWT_REALLY\"}", ) .expect("failed to deserialize"), ) .expect("JWT type should be allowed but is not"); } // Nested JWTs. assert_unsupported( verifier.validate_jose_header( &serde_json::from_str::("{\"alg\":\"RS256\",\"cty\":\"JWT\"}") .expect("failed to deserialize"), ), "nested JWT", ); assert_unsupported( verifier.validate_jose_header( &serde_json::from_str::( "{\"alg\":\"RS256\",\"cty\":\"NOT_A_JWT\"}", ) .expect("failed to deserialize"), ), "unsupported JWT content type", ); // Critical fields. Adapted from https://tools.ietf.org/html/rfc7515#appendix-E assert_unsupported( verifier.validate_jose_header( &serde_json::from_str::( "{\ \"alg\":\"RS256\",\ \"crit\":[\"http://example.invalid/UNDEFINED\"],\ \"http://example.invalid/UNDEFINED\":true\ }", ) .expect("failed to deserialize"), ), "critical JWT header fields are unsupported", ); } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] struct TestClaims { aud: Option>, iss: Option, payload: String, } impl AudiencesClaim for TestClaims { fn audiences(&self) -> Option<&Vec> { self.aud.as_ref() } } impl IssuerClaim for TestClaims { fn issuer(&self) -> Option<&IssuerUrl> { self.iss.as_ref() } } type TestClaimsJsonWebToken = JsonWebToken< CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, TestClaims, JsonWebTokenJsonPayloadSerde, >; #[test] fn test_jwt_verified_claims() { let rsa_key = serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); let client_id = ClientId::new("my_client".to_string()); let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); let verifier = CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![rsa_key.clone()]), ); // Invalid JOSE header. assert_unsupported( verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJBMjU2R0NNIiwiY3R5IjoiSldUIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Im\ h0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ), "nested JWT", ); // JWE-encrypted JWT. assert_unsupported( verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJBMjU2R0NNIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbX\ BsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ), "JWE encryption", ); // Wrong issuer. match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vYXR0YWNrZXIuY\ 29tIiwicGF5bG9hZCI6ImhlbGxvIHdvcmxkIn0.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::InvalidIssuer(_)) => {} other => panic!("unexpected result: {:?}", other), } // Missing issuer. match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.\ YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::InvalidIssuer(_)) => {} other => panic!("unexpected result: {:?}", other), } // Ignore missing issuer. verifier .clone() .require_issuer_match(false) .verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.\ nv09al63NNDfb8cF3IozegXKbPaUC08zknRPKmQ5qKgXv80hjVxknkpRz7BxocB3JYTBjhYd0gyN9wAuJj\ byZ1QaUC14HOB83awAGbehy5yFLkLadTfPT7-siBCvE2V7AF73a_21YvwdkKmJ-RaKWHzFnG8CDmioma3X\ cWyrsdRLgvUkrWllajLRo8DCIXQ8OuZo1_o4n17PSlPxSkhKIrgaWCvG6tan40Y_1DZOFv47bx4hQUGd-J\ h2aEjiwn65WV3M_Xb2vQMP7VgYNVaNlfxzpL4yDASItbPMWaXBt3ZUa_IOGoSx2GMnPkrQ4xp56qUth6U7\ esWPqRSqqolnHg" .to_string(), )).expect("failed to deserialize"), ).expect("verification should succeed"); // Wrong audience. match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsib3RoZXJfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::InvalidAudience(_)) => {} other => panic!("unexpected result: {:?}", other), } // Missing audience. match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwicGF5bG9hZCI6ImhlbGxvI\ HdvcmxkIn0.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::InvalidAudience(_)) => {} other => panic!("unexpected result: {:?}", other), } // Ignore missing audience. verifier .clone() .require_audience_match(false) .verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwicGF5bG9hZCI6Imhlb\ GxvIHdvcmxkIn0.lP-Z_zGPNoKIbLQsnrZc2LAc5qJrKyb7t07ZtJUKVhcwHiCUou4bBhq5RHlElCh\ 0ElRRP6I25lp6UszkRvIC46UV3GVze0x73kVkHSvCVI7MO75LbL9BRqrm5b4CN2zCiFBY8-EwTXnJd\ Ri0d_U8K29TV24L2I-Z5ZILebwUue1N59AGDjx2yYLFx5NOw3TUsPyscG62aZAT321pL_jcYwTWTWw\ 2FYm07zguwx-PUTZwGXlJiOgXQqRIbY_1bS3I_D8UWsmEB3DmV0f9z-iklgIPFawa4wHaE-hpzBAEx\ pSieyOavA5pl0Se3XRYA-CkdDVgzG0Pt4IdnxFanfUXTw" .to_string(), )) .expect("failed to deserialize"), ) .expect("verification should succeed"); // Multiple audiences, where one is a match (default = reject) match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsIm15X2NsaWVudCIsImF1ZDIiXSwiaXNzIjoia\ HR0cHM6Ly9leGFtcGxlLmNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.N9ibisEe0kKLe1GDWM\ ON3PmYqbL73dag-loM8pjKJNinF9SB7n4JuSu4FrNkeW4F1Cz8MIbLuWfKvDa_4v_3FstMA3GODZWH\ BVIiuNFay2ovCfGFyykwe47dF_47g_OM5AkJc_teE5MN8lPh9V5zYCy3ON3zZ3acFPJMOPTdbU56xD\ eFe7lil6DmV4JU9A52t5ZkJILFaIuxxXJUIDmqpPTvHkggh_QOj9C2US9bgg5b543JwT4j-HbDp51L\ dDB4k3azOssT1ddtoAuuDOctnraMKUtqffJXexxfwA1uM6EIofSrK5v11xwgTciL9xDXAvav_G2buP\ ol1bjGLa2t0Q" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::InvalidAudience(_)) => {} other => panic!("unexpected result: {:?}", other), } // Multiple audiences, where one is a match (allowed) verifier .clone() .set_other_audience_verifier_fn(|aud| **aud == "aud1" || **aud == "aud2") .verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsIm15X2NsaWVudCIsImF1ZDIiXSwiaXNzIjoia\ HR0cHM6Ly9leGFtcGxlLmNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.N9ibisEe0kKLe1GDWM\ ON3PmYqbL73dag-loM8pjKJNinF9SB7n4JuSu4FrNkeW4F1Cz8MIbLuWfKvDa_4v_3FstMA3GODZWH\ BVIiuNFay2ovCfGFyykwe47dF_47g_OM5AkJc_teE5MN8lPh9V5zYCy3ON3zZ3acFPJMOPTdbU56xD\ eFe7lil6DmV4JU9A52t5ZkJILFaIuxxXJUIDmqpPTvHkggh_QOj9C2US9bgg5b543JwT4j-HbDp51L\ dDB4k3azOssT1ddtoAuuDOctnraMKUtqffJXexxfwA1uM6EIofSrK5v11xwgTciL9xDXAvav_G2buP\ ol1bjGLa2t0Q" .to_string(), )) .expect("failed to deserialize"), ) .expect("verification should succeed"); // Multiple audiences, where none is a match match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsiYXVkMSIsImF1ZDIiXSwiaXNzIjoiaHR0cHM6Ly9leGFtcGxlL\ mNvbSIsInBheWxvYWQiOiJoZWxsbyB3b3JsZCJ9.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::InvalidAudience(_)) => {} other => panic!("unexpected result: {:?}", other), } // Disable signature check. verifier .clone() .require_signature_check(false) .verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ) .expect("verification should succeed"); // "none" algorithm (unsigned JWT). match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJub25lIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ." .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::NoSignature, )) => {} other => panic!("unexpected result: {:?}", other), } let valid_rs256_jwt = serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.UZ7vmAsDmOBzeB6e2_0POUfyhMRZKM6WSKz3\ jB2QdmO-eZ9605EzhkJufJQ8515ryWnHv-gUHtZHQi3zilrzhBwvE2cVP83Gv2XIL1EKaMMmfISeEB\ ShWez_FvqxN_bamh5yTROhWmoZTmof-MweBCHgINcsEd7K4e_BHHgq3aaRBpvSFlL_z4l_1NwNcTBo\ kqjNScKZITk42AbsSuGR39L94BWLhz6WXQZ_Sn6R1Ro6roOm1b7E82jJiQEtlseQiCCvPR2JJ6LgW6\ XTMzQ0vCqSh1A7U_IBDsjY_yag8_X3xxFh2URCtHJ47ZSjqfv6hq7OAq8tmVecOVgfIvABOg" .to_string(), )) .expect("failed to deserialize"); // Default algs + RS256 -> allowed verifier .verified_claims(valid_rs256_jwt.clone()) .expect("verification should succeed"); let verifier_with_client_secret = CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![]), ) .set_client_secret(ClientSecret::new("my_secret".to_string())); let valid_hs256_jwt = serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.dTXvSWen74_rC4oiWw0ziLZNe4KZk8Jw2VZe\ N6vLCDo" .to_string(), )) .expect("failed to deserialize"); // Default algs + HS256 -> disallowed match verifier_with_client_secret.verified_claims(valid_hs256_jwt.clone()) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::DisallowedAlg(_), )) => {} other => panic!("unexpected result: {:?}", other), } // none algs + RS256 -> allowed verifier .clone() .allow_any_alg() .verified_claims(valid_rs256_jwt.clone()) .expect("verification should succeed"); // none algs + HS256 -> allowed verifier_with_client_secret .clone() .allow_any_alg() .verified_claims(valid_hs256_jwt.clone()) .expect("verification should succeed"); // none algs + none -> disallowed match verifier.clone().allow_any_alg().verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJub25lIn0.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ." .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::NoSignature, )) => {} other => panic!("unexpected result: {:?}", other), } // HS256 + no client secret -> disallowed match verifier .clone() .allow_any_alg() .verified_claims(valid_hs256_jwt.clone()) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::DisallowedAlg(_), )) => {} other => panic!("unexpected result: {:?}", other), } // HS256 + valid signature verifier_with_client_secret .clone() .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) .verified_claims(valid_hs256_jwt) .expect("verification should succeed"); // HS256 + invalid signature match verifier_with_client_secret .clone() .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) .verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.dTXvSWen74_rC4oiWw0ziLZNe4KZk8Jw2VZe\ N6vLCEo" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::CryptoError(_), )) => {} other => panic!("unexpected result: {:?}", other), } // No public keys match CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![]), ) .verified_claims(valid_rs256_jwt.clone()) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::NoMatchingKey, )) => {} other => panic!("unexpected result: {:?}", other), } let kid = JsonWebKeyId::new("bilbo.baggins@hobbiton.example".to_string()); let n = Base64UrlEncodedBytes::new(vec![ 159, 129, 15, 180, 3, 130, 115, 208, 37, 145, 228, 7, 63, 49, 210, 182, 0, 27, 130, 206, 219, 77, 146, 240, 80, 22, 93, 71, 207, 202, 184, 163, 196, 28, 183, 120, 172, 117, 83, 121, 63, 142, 249, 117, 118, 141, 26, 35, 116, 216, 113, 37, 100, 195, 188, 215, 123, 158, 164, 52, 84, 72, 153, 64, 124, 255, 0, 153, 146, 10, 147, 26, 36, 196, 65, 72, 82, 171, 41, 189, 176, 169, 92, 6, 83, 243, 108, 96, 230, 11, 249, 11, 98, 88, 221, 165, 111, 55, 4, 123, 165, 194, 209, 208, 41, 175, 156, 157, 64, 186, 199, 170, 65, 199, 138, 13, 209, 6, 138, 221, 105, 158, 128, 143, 234, 1, 30, 161, 68, 29, 138, 79, 123, 180, 233, 123, 227, 159, 85, 241, 221, 212, 78, 156, 75, 163, 53, 21, 151, 3, 212, 211, 75, 96, 62, 101, 20, 122, 79, 35, 214, 211, 192, 153, 108, 117, 237, 238, 132, 106, 130, 209, 144, 174, 16, 120, 60, 150, 28, 240, 56, 122, 237, 33, 6, 210, 208, 85, 91, 111, 217, 55, 250, 213, 83, 83, 135, 224, 255, 114, 255, 190, 120, 148, 20, 2, 176, 184, 34, 234, 42, 116, 182, 5, 140, 29, 171, 249, 179, 74, 118, 203, 99, 184, 127, 170, 44, 104, 71, 184, 226, 131, 127, 255, 145, 24, 110, 107, 28, 20, 145, 28, 249, 137, 168, 144, 146, 168, 28, 230, 1, 221, 172, 211, 249, 207, ]); let e = Base64UrlEncodedBytes::new(vec![1, 0, 1]); // Wrong key type (symmetric key) match CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![CoreJsonWebKey { kty: CoreJsonWebKeyType::Symmetric, use_: Some(CoreJsonWebKeyUse::Signature), kid: Some(kid.clone()), n: None, e: None, k: Some(Base64UrlEncodedBytes::new(vec![1, 2, 3, 4])), crv: None, x: None, y: None, d: None, alg: None, }]), ) .verified_claims(valid_rs256_jwt.clone()) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::NoMatchingKey, )) => {} other => panic!("unexpected result: {:?}", other), } // Correct public key, but with signing disallowed match CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![CoreJsonWebKey { kty: CoreJsonWebKeyType::RSA, use_: Some(CoreJsonWebKeyUse::Encryption), kid: Some(kid), n: Some(n), e: Some(e), k: None, crv: None, x: None, y: None, d: None, alg: None, }]), ) .verified_claims(valid_rs256_jwt.clone()) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::NoMatchingKey, )) => {} other => panic!("unexpected result: {:?}", other), } // Wrong key ID match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiIsImtpZCI6Indyb25nX2tleSJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6I\ mh0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.lVLomyIyO8WmyS1VZWPu\ cGhRTUyK9RCw90fJC5CfDWUCgt1CBn-aP_ieWWBGfjb4ccR4dl57OYxdLl0Day8QN5pTCBud9QKpQ0rKQX\ K8eBlOW8uSosx8q5pwU_bRyy-XuKJiPlDCOwTEHOp_hOgZFGjoN27MH3Xm8kc0iT3PgyqQ46-wsqHY9S02\ hdJORX7vqYwQLZF8_k_L8K0IG_dC-1Co0g5oAf37oVSdl8hE-ScQ9K-AiSpS-cGYyldbMhyKNDL3ry2cuI\ EUgYSIznkVFuM7RrEdNK222z5PF11ijYx-TM7BIDggbcIyJm-UqpmvVaJImmj5FNkMzuHYznLtdg" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::NoMatchingKey, )) => {} other => panic!("unexpected result: {:?}", other), } // Client secret + public key verifier .clone() .set_client_secret(ClientSecret::new("my_secret".to_string())) .verified_claims(valid_rs256_jwt.clone()) .expect("verification should succeed"); // Multiple matching public keys: no KID specified match CoreJwtClaimsVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![rsa_key.clone(), rsa_key.clone()]), ) .verified_claims(valid_rs256_jwt.clone()) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::AmbiguousKeyId(_), )) => {} other => panic!("unexpected result: {:?}", other), } // Multiple matching public keys: KID specified match CoreJwtClaimsVerifier::new( client_id, issuer, CoreJsonWebKeySet::new(vec![rsa_key.clone(), rsa_key]), ) .verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.eyJhdWQiO\ lsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJwYXlsb2FkIjoiaGVsbG8gd29\ ybGQifQ.jH0v2fQGvH2MD0jn5pQP6W6AF5rJlizyofdyRUIt7E3GraGA1LYDiLAVIfhST3uwJopP-TgtBk\ zc-zyJSvgTR63S8iI1YlHypItpx7r4I9ydzo8GSN5RrZudcU2esY4uEnLbVl17ZVNu4IyTExeKJ0sPM0Hj\ qkOA4XaP2cJwsK-bookNHSA8NRE6adRMrHAKJbor5jrGjpkZAKHbnQFK-wu-nEV_OjS9jpN_FboRZVcDTZ\ GFzeFbqFqHdRn6UWPFnVpVnUhih16UjNH1om6gwc0uFoPWTDxJlXQCFbHMhZtgCbUkXQBH7twPMc4YUziw\ S8GIRKCcXjdrP5oyxmcitQ" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::AmbiguousKeyId(_), )) => {} other => panic!("unexpected result: {:?}", other), } // RS256 + valid signature verifier .verified_claims(valid_rs256_jwt) .expect("verification should succeed"); // RS256 + invalid signature match verifier.verified_claims( serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb\ 20iLCJwYXlsb2FkIjoiaGVsbG8gd29ybGQifQ.YmFkX2hhc2g" .to_string(), )) .expect("failed to deserialize"), ) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::CryptoError(_), )) => {} other => panic!("unexpected result: {:?}", other), } } type CoreIdTokenJwt = JsonWebToken< CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreIdTokenClaims, JsonWebTokenJsonPayloadSerde, >; #[test] fn test_id_token_verified_claims() { let rsa_key = serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); let client_id = ClientId::new("my_client".to_string()); let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); let mock_current_time = AtomicUsize::new(1544932149); let mock_is_valid_issue_time = AtomicBool::new(true); // Extra scope needed to ensure closures are destroyed before the values they borrow. { let public_client_verifier = CoreIdTokenVerifier::new_public_client( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![rsa_key.clone()]), ) .set_time_fn(|| { Timestamp::Seconds(mock_current_time.load(Ordering::Relaxed).into()) .to_utc() .unwrap() }) .set_issue_time_verifier_fn(|_| { if mock_is_valid_issue_time.load(Ordering::Relaxed) { Ok(()) } else { Err("Invalid iat claim".to_string()) } }); let insecure_verifier = CoreIdTokenVerifier::new_insecure_without_verification() .set_time_fn(|| { Timestamp::Seconds(mock_current_time.load(Ordering::Relaxed).into()) .to_utc() .unwrap() }); // This JWTs below have an issue time of 1544928549 and an expiration time of 1544932149. let test_jwt_without_nonce = serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDl9.nN\ aTxNwclnTHd1Q9POkddm5wB1w3wJ-gwQWHomhimttk3SWQTLhxI0SSjWrHahGxlfkjufJlSyt-t_VO\ SdcROvIYZTDznDfFZz3oSOev-p9XiZ-EZTS-U6N11Y923sDQjbTMeukz1F3ZFEfn5Mv2xjdEoJccCe\ 7SaGuDmVqMqTLXMtsw9NCE_KDd0oKSwDzbJIBBPEfG3JjbKg0Dln7ENHg9wzoNFQzPXrkKzjneBgD3\ vuwFCV5y-e8xUBdLaLZF1kdkDZJIA48uRROLlWjsM8pEptosA5QK07luQCZNqcaZWEczoGXeQs8PyA\ zkNV7JEmti3bJnWSN-ud4cFU0LiQ" .to_string(), )) .expect("failed to deserialize"); // Invalid JWT claims match public_client_verifier.verified_claims( &serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vYXR0YWNrZ\ XIuY29tIiwic3ViIjoic3ViamVjdCIsImV4cCI6MTU0NDkzMjE0OSwiaWF0IjoxNTQ0OTI4NTQ5LCJ\ ub25jZSI6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IifQ.Pkicxk0dTU5BkSxgqTON6lE7A7ir3l\ aADRyoeRoCNDX3AOx7BXCbfzbda6HJiPskN2nu56w0q-0OdkDSIHls-2xTUlLEJv2Bv0BLYwV5ZVJ8\ hoc-rTd0_oLUb5NzyD80RyVByjVMK8bh6cwysTnr8QDxsEiFZbFo3mVJob2yjPZnNOdcNJWPcVVueP\ 8vqMJnx5kHih1gKZpWj_dMN9b2AW6zVLOInW3Ox__gx6fsFFz7rjxItG-PTY_OQMzthqeHUyq4o9y7\ Jv8mB_jFkTZGVKHTPpObHV-qptJ_rnlwvF_mP5GARBLng-4Yd7nmSr31onYL48QDjGOrwPqQ-IyaCQ" .to_string(), )) .expect("failed to deserialize"), |_: Option<&Nonce>| Ok(())) { Err(ClaimsVerificationError::InvalidIssuer(_)) => {} other => panic!("unexpected result: {:?}", other), } // TODO: disallowed algs // Expired token mock_current_time.store(1544928549 + 3600, Ordering::Relaxed); match public_client_verifier .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) { Err(ClaimsVerificationError::Expired(_)) => {} other => panic!("unexpected result: {:?}", other), } mock_current_time.store(1544928549 + 1, Ordering::Relaxed); // Invalid issue time mock_is_valid_issue_time.store(false, Ordering::Relaxed); match public_client_verifier .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) { Err(ClaimsVerificationError::Expired(_)) => {} other => panic!("unexpected result: {:?}", other), } mock_is_valid_issue_time.store(true, Ordering::Relaxed); let valid_nonce = Nonce::new("the_nonce".to_string()); // Successful verification w/o checking nonce public_client_verifier .verified_claims(&test_jwt_without_nonce, |_: Option<&Nonce>| Ok(())) .expect("verification should succeed"); // Missing nonce match public_client_verifier.verified_claims(&test_jwt_without_nonce, &valid_nonce) { Err(ClaimsVerificationError::InvalidNonce(_)) => {} other => panic!("unexpected result: {:?}", other), } // Missing nonce w/ closure match public_client_verifier.verified_claims( &test_jwt_without_nonce, |nonce: Option<&Nonce>| { if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { Ok(()) } else { Err("invalid nonce".to_string()) } }, ) { Err(ClaimsVerificationError::InvalidNonce(_)) => {} other => panic!("unexpected result: {:?}", other), } let test_jwt_with_nonce = serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ vbmNlIjoidGhlX25vbmNlIiwiYWNyIjoidGhlX2FjciIsImF1dGhfdGltZSI6MTU0NDkyODU0OH0.W\ XA7SS9aMh_6rvBEgQce5D2J84OqphmmnCLGgEKRTN5G-UuQTNOBp8VS5_4f3xgzMEEMvGJJauJoALk\ muUeHB-N_ESrkmB3tgDzBSYBa7kuYPHUPYpdjZM2UVolqI9RYyHaWwKjL_Io5YyAazB5lH5ibPaiBl\ UNKGs3cmVsEB22UGMFKM6cek7GinrHQe_aJQsMU839-c2zzlEyFSeI8QBphQtG6AN82IPkNRv8QWmw\ ZjUiB5a-W73Z3gURYMNs7f32BjAUNoJzW0Qj34vzD2djoSHhltE0wHKBzPqGhUM1Y3A-a3q-LS2g1h\ 6qgXb_KQ_Mmok8v8ld0cW_aYRLfNg" .to_string(), )) .expect("failed to deserialize"); // Invalid nonce match public_client_verifier.verified_claims( &test_jwt_with_nonce, &Nonce::new("different_nonce".to_string()), ) { Err(ClaimsVerificationError::InvalidNonce(_)) => {} other => panic!("unexpected result: {:?}", other), } let verified_claims = public_client_verifier .clone() .set_auth_context_verifier_fn(|acr| { assert_eq!(**acr.unwrap(), "the_acr"); Err("Invalid acr claim".to_string()) }) .verified_claims(&test_jwt_with_nonce, &valid_nonce); // Invalid AuthenticationContextClass reference match verified_claims { Err(ClaimsVerificationError::InvalidAuthContext(_)) => {} other => panic!("unexpected result: {:?}", other), } let test_jwt_without_auth_time = serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ vbmNlIjoidGhlX25vbmNlIiwiYWNyIjoidGhlX2FjciJ9.c_lU1VRasTg0mB4lwdOzbzvFS_XShMLN\ lAPUpHBaMtCSPtI71L2x3hIByfkqIrAED-Qc_am2gNJ20bifidlkTOO6nyaBrJuaSjwT8aqajEbXon\ 5JFswwPvqCIWjd0eV5dXC1MZunpd7ANXSC7Qw16v3m_crc9wcI_fLFCzuAKrWYokGvNy0gr1CxcgVg\ aE9qR0eqaatetzCuaOJhYOq4njrRlGZWtbj5Q56q3zhxJ_yS8K8gv1QcB4sHjUyXIj21jzjUD87zVG\ dJsn8E-nFJSltBdQhEaLksTBH6ZZhkeGicQ8cEPnNeS4L1vfVyAd_cjl64JHLmzw8RUp8XuoF9nA" .to_string(), )) .expect("failed to deserialize"); // Missing auth_time (ok) public_client_verifier .verified_claims(&test_jwt_without_auth_time, |_: Option<&Nonce>| Ok(())) .expect("verification should succeed"); let verified_claims = public_client_verifier .clone() .set_auth_time_verifier_fn(|auth_time| { assert!(auth_time.is_none()); Err("Invalid auth_time claim".to_string()) }) .verified_claims(&test_jwt_without_auth_time, |_: Option<&Nonce>| Ok(())); // Missing auth_time (error) match verified_claims { Err(ClaimsVerificationError::InvalidAuthTime(_)) => {} other => panic!("unexpected result: {:?}", other), } let verified_claims = public_client_verifier .clone() .set_auth_time_verifier_fn(|auth_time| { assert_eq!( auth_time.unwrap(), Timestamp::Seconds(1544928548.into()).to_utc().unwrap(), ); Err("Invalid auth_time claim".to_string()) }) .verified_claims(&test_jwt_with_nonce, &valid_nonce); // Invalid auth_time match verified_claims { Err(ClaimsVerificationError::InvalidAuthTime(_)) => {} other => panic!("unexpected result: {:?}", other), } // Successful verification with nonce, acr, and auth_time specified (no expected Nonce) public_client_verifier .verified_claims(&test_jwt_with_nonce, |_: Option<&Nonce>| Ok(())) .expect("verification should succeed"); insecure_verifier .verified_claims(&test_jwt_with_nonce, |_: Option<&Nonce>| Ok(())) .expect("verification should succeed"); // Successful verification with nonce, acr, and auth_time specified (w/ expected Nonce) public_client_verifier .verified_claims(&test_jwt_with_nonce, &valid_nonce) .expect("verification should succeed"); insecure_verifier .verified_claims(&test_jwt_with_nonce, &valid_nonce) .expect("verification should succeed"); // Successful verification with nonce, acr, and auth_time specified (w/ closure) public_client_verifier .verified_claims(&test_jwt_with_nonce, |nonce: Option<&Nonce>| { if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { Ok(()) } else { Err("invalid nonce".to_string()) } }) .expect("verification should succeed"); insecure_verifier .verified_claims(&test_jwt_with_nonce, |nonce: Option<&Nonce>| { if nonce.iter().any(|n| n.secret() == valid_nonce.secret()) { Ok(()) } else { Err("invalid nonce".to_string()) } }) .expect("verification should succeed"); // HS256 w/ default algs let test_jwt_hs256 = serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhbXBsZ\ S5jb20iLCJzdWIiOiJzdWJqZWN0IiwiZXhwIjoxNTQ0OTMyMTQ5LCJpYXQiOjE1NDQ5Mjg1NDksIm5\ vbmNlIjoidGhlX25vbmNlIn0.xUnSwSbcHsHWyJxwKGg69BIo_CktcyN5BVulGDb_QzE" .to_string(), )) .expect("failed to deserialize"); let private_client_verifier = CoreIdTokenVerifier::new_confidential_client( client_id.clone(), ClientSecret::new("my_secret".to_string()), issuer.clone(), CoreJsonWebKeySet::new(vec![rsa_key.clone()]), ) .set_time_fn(|| { Timestamp::Seconds(mock_current_time.load(Ordering::Relaxed).into()) .to_utc() .unwrap() }); match private_client_verifier.verified_claims(&test_jwt_hs256, &valid_nonce) { Err(ClaimsVerificationError::SignatureVerification(_)) => {} other => panic!("unexpected result: {:?}", other), } insecure_verifier .clone() .verified_claims(&test_jwt_hs256, &valid_nonce) .expect("verification should succeed"); // HS256 w/ set_allowed_algs private_client_verifier .clone() .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]) .verified_claims(&test_jwt_hs256, &valid_nonce) .expect("verification should succeed"); // HS256 w/ allow_any_alg private_client_verifier .clone() .allow_any_alg() .verified_claims(&test_jwt_hs256, &valid_nonce) .expect("verification should succeed"); // Invalid signature let private_client_verifier_with_other_secret = CoreIdTokenVerifier::new_confidential_client( client_id, ClientSecret::new("other_secret".to_string()), issuer, CoreJsonWebKeySet::new(vec![rsa_key]), ) .allow_any_alg() .set_time_fn(|| { Timestamp::Seconds(mock_current_time.load(Ordering::Relaxed).into()) .to_utc() .unwrap() }); match private_client_verifier_with_other_secret .verified_claims(&test_jwt_hs256, &valid_nonce) { Err(ClaimsVerificationError::SignatureVerification(_)) => {} other => panic!("unexpected result: {:?}", other), } // Invalid signature w/ signature check disabled private_client_verifier_with_other_secret .clone() .insecure_disable_signature_check() .verified_claims(&test_jwt_hs256, &valid_nonce) .expect("verification should succeed"); }; } #[test] fn test_new_id_token() { let client_id = ClientId::new("my_client".to_string()); let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); let nonce = Nonce::new("the_nonce".to_string()); let rsa_priv_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_PRIV_KEY, None).unwrap(); let id_token = CoreIdToken::new( CoreIdTokenClaims::new( issuer.clone(), vec![Audience::new((*client_id).clone())], Utc.timestamp_opt(1544932149, 0) .single() .expect("valid timestamp"), Utc.timestamp_opt(1544928549, 0) .single() .expect("valid timestamp"), StandardClaims::new(SubjectIdentifier::new("subject".to_string())), Default::default(), ) .set_nonce(Some(nonce.clone())) .set_auth_context_ref(Some(AuthenticationContextClass::new("the_acr".to_string()))) .set_auth_time(Some( Utc.timestamp_opt(1544928548, 0) .single() .expect("valid timestamp"), )), &rsa_priv_key, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, Some(&AccessToken::new("the_access_token".to_string())), Some(&AuthorizationCode::new( "the_authorization_code".to_string(), )), ) .unwrap(); let serialized_jwt: serde_json::Value = serde_json::to_value(&id_token).unwrap(); let expected_serialized_jwt = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjpbIm15X2NsaWVudCJdL\ CJleHAiOjE1NDQ5MzIxNDksImlhdCI6MTU0NDkyODU0OSwiYXV0aF90aW1lIjoxNTQ0OTI4NTQ4LCJub25jZSI\ 6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IiLCJhdF9oYXNoIjoiWjNJQUNVR00tbXhIV3lZUXZpSzhFUSIsI\ mNfaGFzaCI6Imo2OW1CZmFIbmRMM1Y1RmNoak9LVXciLCJzdWIiOiJzdWJqZWN0In0.CHCWFcIqbCZhZwZH4oY\ _mlcRy5aUQQtlNI0VHNYxiILn9ppRHLL4Bn_LMn9VP8tGXkfZWxCgP25ZTyBXXKfk0fQvnukVdyM0bCOpQbiBg\ 5gB9c46l_f-ZznDoHWonpnKky2Gmzk3ocb3TCUQ9GSeRXAzRdRNWTT0ElWNBsLWU4j2IIdnghM78gkXwOC76Rk\ pshgB73ubtuHGdIf5L9Ec3hifHlVjzKuvedAM4SIOjdBOelgtBlF3463ufX_Ut91CjP5TzLMsuK3Lh_vyo8ttn\ S41rBDuetR2ENvR0yj5RjkX_SPY3V0yCW8_NPPu1CHu_1oL0Nma0ohCbF3vnUJcwg"; assert_eq!(expected_serialized_jwt, serialized_jwt.as_str().unwrap()); let rsa_pub_key = serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); let mock_current_time = AtomicUsize::new(1544932148); let time_fn = || { Timestamp::Seconds(mock_current_time.load(Ordering::Relaxed).into()) .to_utc() .unwrap() }; let verifier = CoreIdTokenVerifier::new_public_client( client_id, issuer, CoreJsonWebKeySet::new(vec![rsa_pub_key]), ) .set_time_fn(time_fn); let claims = id_token.claims(&verifier, &nonce).unwrap(); let unverified = id_token .claims( &CoreIdTokenVerifier::new_insecure_without_verification().set_time_fn(time_fn), &nonce, ) .unwrap(); assert_eq!(claims, unverified); } #[test] fn test_user_info_verified_claims() { let rsa_key = serde_json::from_str::(TEST_RSA_PUB_KEY).expect("deserialization failed"); let client_id = ClientId::new("my_client".to_string()); let issuer = IssuerUrl::new("https://example.com".to_string()).unwrap(); let sub = SubjectIdentifier::new("the_subject".to_string()); let verifier = CoreUserInfoVerifier::new( client_id.clone(), issuer.clone(), CoreJsonWebKeySet::new(vec![rsa_key.clone()]), Some(sub.clone()), ); let json_claims = "{\ \"sub\": \"the_subject\",\ \"name\": \"Jane Doe\"\ }"; // JSON response (default args) assert_eq!( CoreUserInfoClaims::from_json::(json_claims.as_bytes(), Some(&sub)) .expect("verification should succeed") .name() .unwrap() .iter() .collect::>(), vec![(None, &EndUserName::new("Jane Doe".to_string()))], ); // Invalid subject match CoreUserInfoClaims::from_json::( json_claims.as_bytes(), Some(&SubjectIdentifier::new("wrong_subject".to_string())), ) { Err(UserInfoError::ClaimsVerification(ClaimsVerificationError::InvalidSubject(_))) => {} other => panic!("unexpected result: {:?}", other), } let jwt_claims = serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhb\ XBsZS5jb20iLCJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKYW5lIERvZSJ9.aX7VpexLAd\ 43HtC1cFTot3jmqsr105rB50mzTcS1TXzWcxLbqYf1K7Kf-S1oP-ZCL_dnL9-nu3iDK_vRa6xT\ nGGt3I1JwhoIv6znSS3JOPT1wtekyD-sLcUwqsJHWBBiTSBwlmGG_kVRuGkBtXgVZ9aGlqg9u1\ FlxvyGUJ5q1o9gdb8mKql5ojgsThTNo9qdW3lPIVsiDO-n4mMp4HuOp1re4ZDDkHxiExjtLQAV\ kR4q3SlhJC2mkr4mw3_0a2AW52ocWDiwY_lPcdmohmwFaB8aHlivYLFnmKGQIatEW-KDaW5fFo\ JYreNkplo4FvzXYyxgxAsqHjHMI8MZVEa1IA" .to_string(), )) .expect("failed to deserialize"); // Valid JWT response (default args) jwt_claims .clone() .claims(&verifier) .expect("verification should succeed"); // JWT response with invalid signature match serde_json::from_value::(serde_json::Value::String( "eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOlsibXlfY2xpZW50Il0sImlzcyI6Imh0dHBzOi8vZXhhb\ XBsZS5jb20iLCJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKYW5lIERvZSJ9.bX7VpexLAd\ 43HtC1cFTot3jmqsr105rB50mzTcS1TXzWcxLbqYf1K7Kf-S1oP-ZCL_dnL9-nu3iDK_vRa6xT\ nGGt3I1JwhoIv6znSS3JOPT1wtekyD-sLcUwqsJHWBBiTSBwlmGG_kVRuGkBtXgVZ9aGlqg9u1\ FlxvyGUJ5q1o9gdb8mKql5ojgsThTNo9qdW3lPIVsiDO-n4mMp4HuOp1re4ZDDkHxiExjtLQAV\ kR4q3SlhJC2mkr4mw3_0a2AW52ocWDiwY_lPcdmohmwFaB8aHlivYLFnmKGQIatEW-KDaW5fFo\ JYreNkplo4FvzXYyxgxAsqHjHMI8MZVEa1IA" .to_string(), )) .expect("failed to deserialize") .claims(&verifier) { Err(ClaimsVerificationError::SignatureVerification( SignatureVerificationError::CryptoError(_), )) => {} other => panic!("unexpected result: {:?}", other), } // JWT response with invalid issuer claim (error) match jwt_claims.clone().claims(&CoreUserInfoVerifier::new( client_id.clone(), IssuerUrl::new("https://attacker.com".to_string()).unwrap(), CoreJsonWebKeySet::new(vec![rsa_key.clone()]), Some(sub.clone()), )) { Err(ClaimsVerificationError::InvalidIssuer(_)) => {} other => panic!("unexpected result: {:?}", other), } // JWT response with invalid issuer claim (allowed) jwt_claims .clone() .claims( &CoreUserInfoVerifier::new( client_id, IssuerUrl::new("https://attacker.com".to_string()).unwrap(), CoreJsonWebKeySet::new(vec![rsa_key.clone()]), Some(sub.clone()), ) .require_issuer_match(false), ) .expect("verification should succeed"); // JWT response with invalid audience claim (error) match jwt_claims.clone().claims(&CoreUserInfoVerifier::new( ClientId::new("wrong_client".to_string()), issuer.clone(), CoreJsonWebKeySet::new(vec![rsa_key.clone()]), Some(sub.clone()), )) { Err(ClaimsVerificationError::InvalidAudience(_)) => {} other => panic!("unexpected result: {:?}", other), } // JWT response with invalid audience claim (allowed) jwt_claims .claims( &CoreUserInfoVerifier::new( ClientId::new("wrong_client".to_string()), issuer, CoreJsonWebKeySet::new(vec![rsa_key]), Some(sub), ) .require_audience_match(false), ) .expect("verification should succeed"); } #[test] fn test_new_user_info_claims() { let claims = CoreUserInfoClaims::new( StandardClaims { sub: SubjectIdentifier::new("the_subject".to_string()), name: Some(EndUserName::new("John Doe".to_string()).into()), given_name: None, family_name: None, middle_name: None, nickname: None, preferred_username: None, profile: None, picture: None, website: None, email: None, email_verified: None, gender: None, birthday: None, birthdate: None, zoneinfo: None, locale: None, phone_number: None, phone_number_verified: None, address: None, updated_at: Some( Utc.timestamp_opt(1544928548, 0) .single() .expect("valid timestamp"), ), }, Default::default(), ); assert_eq!( "{\"sub\":\"the_subject\",\"name\":\"John Doe\",\"updated_at\":1544928548}", serde_json::to_string(&claims).unwrap() ); let rsa_priv_key = CoreRsaPrivateSigningKey::from_pem(TEST_RSA_PRIV_KEY, None).unwrap(); let claims_jwt = CoreUserInfoJsonWebToken::new( claims, &rsa_priv_key, CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, ) .unwrap(); assert_eq!( "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0aGVfc3ViamVjdCIsIm5hbWUiOiJKb2huIERvZSIsInVwZGF0ZWRfY\ XQiOjE1NDQ5Mjg1NDh9.nJ7Buckt_p_ACXkyVRCQLqyaW8KhDsk5H9Nu7PdNf4daEcEWm-lGjoSTAfAbDPgHAZ\ 78knomgLgDxiGWrj1qdFTIEFep32I3q18VBP_DcMdyuQafipK6T98RgZFWP8YnxlxLPHeJQlRsdMpemHK4vxas\ ZD4A4aIn0K7z5J9RvrR3L7DWnc3fJQ0VU2v5QLePyqNWnFxks5eyl8Ios8JrZhwr4Q8GES8Q4Iw8Sz6W9vYpHK\ 2r1YdaACMM4g_TTtV91lpjn-Li2-HxW9NERdLvYvF6HwGIwbss26trp2yjNTARlxBUT6LR7y82oPIJKXIKL1GD\ YeSLeErhb6oTQ0a5gQ", serde_json::to_value(claims_jwt).unwrap().as_str().unwrap() ); } ================================================ FILE: tests/rp_certification_code.rs ================================================ #![allow(clippy::expect_fun_call)] use crate::rp_common::{ get_provider_metadata, http_client, init_log, issuer_url, register_client, PanicIfFail, }; use http::header::LOCATION; use http::method::Method; use log::{debug, error, info}; use openidconnect::core::{ CoreClient, CoreClientAuthMethod, CoreClientRegistrationRequest, CoreClientRegistrationResponse, CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKeySet, CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreResponseType, CoreUserInfoClaims, }; use openidconnect::Nonce; use openidconnect::{ AccessToken, AuthType, AuthenticationFlow, AuthorizationCode, ClaimsVerificationError, CsrfToken, EndpointMaybeSet, EndpointNotSet, EndpointSet, HttpClientError, OAuth2TokenResponse, RequestTokenError, Scope, SignatureVerificationError, UserInfoError, }; use reqwest::{blocking::Client, redirect::Policy}; use url::Url; use std::collections::HashMap; mod rp_common; struct TestState { access_token: Option, authorization_code: Option, client: CoreClient< EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointMaybeSet, EndpointMaybeSet, >, id_token: Option, nonce: Option, provider_metadata: CoreProviderMetadata, registration_response: CoreClientRegistrationResponse, } impl TestState { pub fn init(test_id: &'static str, reg_request_fn: F) -> Self where F: FnOnce(CoreClientRegistrationRequest) -> CoreClientRegistrationRequest, { init_log(test_id); let _issuer_url = issuer_url(test_id); let provider_metadata = get_provider_metadata(test_id); let registration_response = register_client(&provider_metadata, reg_request_fn); let redirect_uri = registration_response.redirect_uris()[0].clone(); let client: CoreClient<_, _, _, _, _, _> = CoreClient::from_provider_metadata( provider_metadata.clone(), registration_response.client_id().to_owned(), registration_response.client_secret().cloned(), ) .set_redirect_uri(redirect_uri); TestState { access_token: None, authorization_code: None, client, id_token: None, nonce: None, provider_metadata, registration_response, } } pub fn access_token(&self) -> &AccessToken { self.access_token.as_ref().expect("no access_token") } pub fn authorize(mut self, scopes: &[Scope]) -> Self { let (authorization_code, nonce) = { let mut authorization_request = self.client.authorize_url( AuthenticationFlow::AuthorizationCode::, CsrfToken::new_random, Nonce::new_random, ); authorization_request = scopes .iter() .fold(authorization_request, |mut authorization_request, scope| { authorization_request = authorization_request.add_scope(scope.clone()); authorization_request }); let (url, state, nonce) = authorization_request.url(); log_debug!("Authorize URL: {:?}", url); let http_client = Client::builder().redirect(Policy::none()).build().unwrap(); let redirect_response = http_client .execute( http_client .request(Method::GET, url.as_str()) .build() .unwrap(), ) .unwrap(); assert!(redirect_response.status().is_redirection()); let redirected_url = Url::parse( redirect_response .headers() .get(LOCATION) .unwrap() .to_str() .unwrap(), ) .unwrap(); log_debug!("Authorization Server redirected to: {:?}", redirected_url); let mut query_params = HashMap::new(); redirected_url.query_pairs().for_each(|(key, value)| { query_params.insert(key, value); }); log_debug!( "Authorization Server returned query params: {:?}", query_params ); assert_eq!( self.provider_metadata.issuer().as_str(), query_params.get("iss").unwrap() ); assert_eq!(state.secret(), query_params.get("state").unwrap()); log_info!("Successfully received authentication response from Authorization Server"); let authorization_code = AuthorizationCode::new(query_params.get("code").unwrap().to_string()); log_debug!( "Authorization Server returned authorization code: {}", authorization_code.secret() ); (authorization_code, nonce) }; self.authorization_code = Some(authorization_code); self.nonce = Some(nonce); self } pub fn exchange_code(mut self) -> Self { let token_response = self .client .exchange_code( self.authorization_code .take() .expect("no authorization_code"), ) .panic_if_fail("should have a token endpoint") .request(&http_client) .panic_if_fail("failed to exchange authorization code for token"); log_debug!( "Authorization Server returned token response: {:?}", token_response ); self.access_token = Some(token_response.access_token().clone()); let id_token = (*token_response .extra_fields() .id_token() .expect("no id_token")) .clone(); self.id_token = Some(id_token); self } pub fn id_token(&self) -> &CoreIdToken { self.id_token.as_ref().expect("no id_token") } pub fn id_token_verifier(&self, jwks: CoreJsonWebKeySet) -> CoreIdTokenVerifier { CoreIdTokenVerifier::new_confidential_client( self.registration_response.client_id().clone(), self.registration_response .client_secret() .expect("no client_secret") .clone(), self.provider_metadata.issuer().clone(), jwks, ) } pub fn id_token_claims(&self) -> &CoreIdTokenClaims { let verifier = self.id_token_verifier(self.jwks()); self.id_token() .claims(&verifier, self.nonce.as_ref().expect("no nonce")) .panic_if_fail("failed to validate claims") } pub fn id_token_claims_failure(&self) -> ClaimsVerificationError { let verifier = self.id_token_verifier(self.jwks()); self.id_token() .claims(&verifier, self.nonce.as_ref().expect("no nonce")) .expect_err("claims verification succeeded but was expected to fail") } pub fn jwks(&self) -> CoreJsonWebKeySet { CoreJsonWebKeySet::fetch(self.provider_metadata.jwks_uri(), &http_client) .panic_if_fail("failed to fetch JWK set") } pub fn set_auth_type(mut self, auth_type: AuthType) -> Self { self.client = self.client.set_auth_type(auth_type); self } pub fn user_info_claims(&self) -> CoreUserInfoClaims { self.client .user_info( self.access_token().to_owned(), Some(self.id_token_claims().subject().clone()), ) .unwrap() .require_signed_response(false) .request(&http_client) .panic_if_fail("failed to get UserInfo") } pub fn user_info_claims_failure(&self) -> UserInfoError> { let user_info_result: Result = self .client .user_info( self.access_token().to_owned(), Some(self.id_token_claims().subject().clone()), ) .unwrap() .require_signed_response(false) .request(&http_client); match user_info_result { Err(err) => err, _ => panic!("claims verification succeeded but was expected to fail"), } } } #[test] #[ignore] fn rp_response_type_code() { let test_state = TestState::init("rp-response_type-code", |reg| reg).authorize(&[]); assert!( test_state .authorization_code .expect("no authorization_code") .secret() != "" ); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_scope_userinfo_claims() { let user_info_scopes = ["profile", "email", "address", "phone"] .iter() .map(|scope| Scope::new((*scope).to_string())) .collect::>(); let test_state = TestState::init("rp-scope-userinfo-claims", |reg| reg) .authorize(&user_info_scopes) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); let user_info_claims = test_state.user_info_claims(); log_debug!("UserInfo response: {:?}", user_info_claims); assert_eq!(id_token_claims.subject(), user_info_claims.subject()); assert!(!user_info_claims .email() .expect("no email returned by UserInfo endpoint") .is_empty()); assert!(!user_info_claims .address() .expect("no address returned by UserInfo endpoint") .street_address .as_ref() .expect("no street address returned by UserInfo endpoint") .is_empty()); assert!(!user_info_claims .phone_number() .expect("no phone_number returned by UserInfo endpoint") .is_empty()); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_nonce_invalid() { let test_state = TestState::init("rp-nonce-invalid", |reg| reg) .authorize(&[]) .exchange_code(); match test_state.id_token_claims_failure() { ClaimsVerificationError::InvalidNonce(_) => { log_error!("ID token contains invalid nonce (expected result)") } other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_token_endpoint_client_secret_basic() { let test_state = TestState::init("rp-token_endpoint-client_secret_basic", |reg| { reg.set_token_endpoint_auth_method(Some(CoreClientAuthMethod::ClientSecretBasic)) }) .set_auth_type(AuthType::BasicAuth) .authorize(&[]) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_token_endpoint_client_secret_post() { let test_state = TestState::init("rp-token_endpoint-client_secret_post", |reg| { reg.set_token_endpoint_auth_method(Some(CoreClientAuthMethod::ClientSecretPost)) }) .set_auth_type(AuthType::RequestBody) .authorize(&[]) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_kid_absent_single_jwks() { let test_state = TestState::init("rp-id_token-kid-absent-single-jwks", |reg| reg) .authorize(&[]) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_iat() { let mut test_state = TestState::init("rp-id_token-iat", |reg| reg).authorize(&[]); let token_response = test_state .client .exchange_code( test_state .authorization_code .take() .expect("no authorization_code"), ) .panic_if_fail("should have a token endpoint") .request(&http_client); match token_response { Err(RequestTokenError::Parse(_, _)) => { log_error!("ID token failed to parse without `iat` claim (expected result)") } other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_aud() { let test_state = TestState::init("rp-id_token-aud", |reg| reg) .authorize(&[]) .exchange_code(); match test_state.id_token_claims_failure() { ClaimsVerificationError::InvalidAudience(_) => { log_error!("ID token has invalid audience (expected result)") } other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_kid_absent_multiple_jwks() { let test_state = TestState::init("rp-id_token-kid-absent-multiple-jwks", |reg| reg) .authorize(&[]) .exchange_code(); match test_state.id_token_claims_failure() { ClaimsVerificationError::SignatureVerification( SignatureVerificationError::AmbiguousKeyId(_), ) => log_error!("ID token has ambiguous key identification without KID (expected result)"), other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_sig_none() { let test_state = TestState::init("rp-id_token-sig-none", |reg| reg) .authorize(&[]) .exchange_code(); let verifier = test_state .id_token_verifier(test_state.jwks()) .insecure_disable_signature_check(); let id_token_claims = test_state .id_token() .claims(&verifier, test_state.nonce.as_ref().expect("no nonce")) .panic_if_fail("failed to validate claims"); log_debug!("ID token: {:?}", id_token_claims); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_sig_rs256() { let test_state = TestState::init("rp-id_token-sig-rs256", |reg| reg) .authorize(&[]) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_sig_hs256() { let test_state = TestState::init("rp-id_token-sig-hs256", |reg| reg) .authorize(&[]) .exchange_code(); let verifier = test_state .id_token_verifier(test_state.jwks()) .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]); let id_token_claims = test_state .id_token() .claims(&verifier, test_state.nonce.as_ref().expect("no nonce")) .panic_if_fail("failed to validate claims"); log_debug!("ID token: {:?}", id_token_claims); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_sub() { let mut test_state = TestState::init("rp-id_token-sub", |reg| reg).authorize(&[]); let token_response = test_state .client .exchange_code( test_state .authorization_code .take() .expect("no authorization_code"), ) .panic_if_fail("should have a token endpoint") .request(&http_client); match token_response { Err(RequestTokenError::Parse(_, _)) => { log_error!("ID token failed to parse without `sub` claim (expected result)") } other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_bad_sig_rs256() { let test_state = TestState::init("rp-id_token-bad-sig-rs256", |reg| reg) .authorize(&[]) .exchange_code(); match test_state.id_token_claims_failure() { ClaimsVerificationError::SignatureVerification( SignatureVerificationError::CryptoError(_), ) => log_error!("ID token has invalid signature (expected result)"), other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_bad_sig_hs256() { let test_state = TestState::init("rp-id_token-bad-sig-hs256", |reg| reg) .authorize(&[]) .exchange_code(); let verifier = test_state .id_token_verifier(test_state.jwks()) .set_allowed_algs(vec![CoreJwsSigningAlgorithm::HmacSha256]); let id_token_err = test_state .id_token() .claims(&verifier, test_state.nonce.as_ref().expect("no nonce")) .expect_err("claims verification succeeded but was expected to fail"); match id_token_err { ClaimsVerificationError::SignatureVerification( SignatureVerificationError::CryptoError(_), ) => log_error!("ID token has invalid signature (expected result)"), other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_id_token_issuer_mismatch() { let test_state = TestState::init("rp-id_token-issuer-mismatch", |reg| reg) .authorize(&[]) .exchange_code(); match test_state.id_token_claims_failure() { ClaimsVerificationError::InvalidIssuer(_) => { log_error!("ID token has invalid issuer (expected result)") } other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_userinfo_bad_sub_claim() { let test_state = TestState::init("rp-userinfo-bad-sub-claim", |reg| reg) .authorize(&[Scope::new("profile".to_string())]) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); match test_state.user_info_claims_failure() { UserInfoError::ClaimsVerification(ClaimsVerificationError::InvalidSubject(_)) => { log_error!("UserInfo response has invalid subject (expected result)") } other => panic!("Unexpected result verifying ID token claims: {:?}", other), } log_info!("SUCCESS"); } #[test] #[ignore] fn rp_userinfo_bearer_header() { let test_state = TestState::init("rp-userinfo-bearer-header", |reg| reg) .authorize(&[Scope::new("profile".to_string())]) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); let user_info_claims = test_state.user_info_claims(); log_debug!("UserInfo response: {:?}", user_info_claims); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_userinfo_sig() { let test_state = TestState::init("rp-userinfo-sig", |reg| { reg.set_userinfo_signed_response_alg(Some(CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256)) }) .authorize(&[Scope::new("profile".to_string())]) .exchange_code(); let id_token_claims = test_state.id_token_claims(); log_debug!("ID token: {:?}", id_token_claims); let user_info_claims: CoreUserInfoClaims = test_state .client .user_info( test_state.access_token().to_owned(), Some(id_token_claims.subject().clone()), ) .unwrap() // For some reason, the test suite omits these claims even though the Core spec says // that the RP SHOULD verify these. .require_audience_match(false) .require_issuer_match(false) .request(&http_client) .panic_if_fail("failed to get UserInfo"); log_debug!("UserInfo response: {:?}", user_info_claims); log_info!("SUCCESS"); } ================================================ FILE: tests/rp_certification_dynamic.rs ================================================ #![allow(clippy::cognitive_complexity)] use crate::rp_common::{ get_provider_metadata, init_log, issuer_url, register_client, CERTIFICATION_BASE_URL, RP_NAME, }; use log::{debug, info}; mod rp_common; #[test] #[ignore] fn rp_discovery_openid_configuration() { const TEST_ID: &str = "rp-discovery-openid-configuration"; init_log(TEST_ID); let _issuer_url = issuer_url(TEST_ID); let provider_metadata = get_provider_metadata(TEST_ID); macro_rules! log_field { ($field:ident) => { log_container_field!(provider_metadata.$field); }; } log_info!( "Successfully retrieved provider metadata from {:?}", _issuer_url ); log_field!(issuer); log_field!(authorization_endpoint); log_field!(token_endpoint); log_field!(userinfo_endpoint); log_field!(jwks_uri); log_field!(registration_endpoint); log_field!(scopes_supported); log_field!(response_types_supported); log_field!(response_modes_supported); log_field!(grant_types_supported); log_field!(acr_values_supported); log_field!(subject_types_supported); log_field!(id_token_signing_alg_values_supported); log_field!(id_token_encryption_alg_values_supported); log_field!(id_token_encryption_enc_values_supported); log_field!(userinfo_signing_alg_values_supported); log_field!(userinfo_encryption_alg_values_supported); log_field!(userinfo_encryption_enc_values_supported); log_field!(request_object_signing_alg_values_supported); log_field!(request_object_encryption_alg_values_supported); log_field!(request_object_encryption_enc_values_supported); log_field!(token_endpoint_auth_methods_supported); log_field!(token_endpoint_auth_signing_alg_values_supported); log_field!(display_values_supported); log_field!(claim_types_supported); log_field!(claims_supported); log_field!(service_documentation); log_field!(claims_locales_supported); log_field!(ui_locales_supported); log_field!(claims_parameter_supported); log_field!(request_parameter_supported); log_field!(request_uri_parameter_supported); log_field!(require_request_uri_registration); log_field!(op_policy_uri); log_field!(op_tos_uri); log_debug!("Provider metadata: {:?}", provider_metadata); log_info!("SUCCESS"); } #[test] #[ignore] fn rp_registration_dynamic() { const TEST_ID: &str = "rp-registration-dynamic"; init_log(TEST_ID); let _issuer_url = issuer_url(TEST_ID); let provider_metadata = get_provider_metadata(TEST_ID); let registration_response = register_client(&provider_metadata, |reg| reg); macro_rules! log_field { ($field:ident) => { log_container_field!(registration_response.$field); }; } log_field!(client_id); log_field!(client_secret); log_field!(registration_access_token); log_field!(registration_client_uri); log_field!(client_id_issued_at); log_field!(client_secret_expires_at); log_field!(redirect_uris); log_field!(response_types); log_field!(grant_types); log_field!(application_type); log_field!(contacts); log_field!(client_name); log_field!(logo_uri); log_field!(client_uri); log_field!(policy_uri); log_field!(tos_uri); log_field!(jwks_uri); log_field!(jwks); log_field!(sector_identifier_uri); log_field!(subject_type); log_field!(id_token_signed_response_alg); log_field!(id_token_encrypted_response_alg); log_field!(id_token_encrypted_response_enc); log_field!(userinfo_signed_response_alg); log_field!(userinfo_encrypted_response_alg); log_field!(userinfo_encrypted_response_enc); log_field!(request_object_signing_alg); log_field!(request_object_encryption_alg); log_field!(request_object_encryption_enc); log_field!(token_endpoint_auth_method); log_field!(token_endpoint_auth_signing_alg); log_field!(default_max_age); log_field!(require_auth_time); log_field!(default_acr_values); log_field!(initiate_login_uri); log_field!(request_uris); log_debug!("Registration response: {:?}", registration_response); assert_eq!( format!( "{}/{}/registration?client_id={}", CERTIFICATION_BASE_URL, RP_NAME, **registration_response.client_id() ), registration_response .registration_client_uri() .unwrap() .to_string() ); log_info!("SUCCESS"); } ================================================ FILE: tests/rp_common.rs ================================================ #![allow(clippy::cognitive_complexity, clippy::expect_fun_call)] use log::{error, warn}; use openidconnect::core::{ CoreApplicationType, CoreClientRegistrationRequest, CoreClientRegistrationResponse, CoreProviderMetadata, }; use openidconnect::{ ClientContactEmail, ClientName, HttpClientError, HttpRequest, HttpResponse, IssuerUrl, RedirectUrl, }; use std::cell::RefCell; use std::sync::Once; use std::time::Duration; pub const CERTIFICATION_BASE_URL: &str = "https://rp.certification.openid.net:8080"; pub const RP_CONTACT_EMAIL: &str = "ramos@cs.stanford.edu"; pub const RP_NAME: &str = "openidconnect-rs"; pub const RP_REDIRECT_URI: &str = "http://localhost:8080"; static INIT_LOG: Once = Once::new(); thread_local! { static TEST_ID: RefCell<&'static str> = const { RefCell::new("UNINITIALIZED_TEST_ID") }; } pub fn get_test_id() -> &'static str { TEST_ID.with(|id| *id.borrow()) } pub fn set_test_id(test_id: &'static str) { TEST_ID.with(|id| *id.borrow_mut() = test_id); } #[macro_export] macro_rules! log_error { ($($args:tt)+) => { error!("[{}] {}", rp_common::get_test_id(), format!($($args)+)) } } #[macro_export] macro_rules! log_info { ($($args:tt)+) => { info!("[{}] {}", rp_common::get_test_id(), format!($($args)+)); } } #[macro_export] macro_rules! log_debug { ($($args:tt)+) => { debug!("[{}] {}", rp_common::get_test_id(), format!($($args)+)); } } #[macro_export] macro_rules! log_container_field { ($container:ident. $field:ident) => { log_info!( concat!(" ", stringify!($field), " = {:?}"), $container.$field() ); }; } fn _init_log() { color_backtrace::install(); env_logger::init(); } pub fn init_log(test_id: &'static str) { INIT_LOG.call_once(_init_log); set_test_id(test_id); } // FIXME: just clone `request` directly once we update `http` to 1.0, which implements `Clone`. #[cfg(feature = "reqwest-blocking")] pub(crate) fn clone_request(request: &HttpRequest) -> HttpRequest { let mut request_copy = http::Request::builder() .method(request.method().to_owned()) .uri(request.uri().to_owned()) .version(request.version()); for (name, value) in request.headers() { request_copy = request_copy.header(name, value); } request_copy.body(request.body().to_owned()).unwrap() } pub fn http_client(request: HttpRequest) -> Result> { retry::retry( (0..5).map(|i| { if i != 0 { warn!("Retrying HTTP request ({}/5)", i + 1) } Duration::from_millis(500) }), || -> Result> { #[cfg(feature = "reqwest-blocking")] { use openidconnect::SyncHttpClient; reqwest::blocking::Client::default().call(clone_request(&request)) } #[cfg(not(feature = "reqwest-blocking"))] { let _ = &request; panic!("reqwest-blocking feature is required") } }, ) .map_err(|err| match err { retry::Error::Operation { error, .. } => error, retry::Error::Internal(msg) => panic!("unexpected error: {msg}"), }) } pub trait PanicIfFail where F: std::error::Error, { fn panic_if_fail(self, msg: &'static str) -> T; } impl PanicIfFail for Result where F: std::error::Error, { fn panic_if_fail(self, msg: &'static str) -> T { match self { Ok(ret) => ret, Err(fail) => { let mut err_msg = format!("Panic: {}", msg); let mut cur_fail: Option<&dyn std::error::Error> = Some(&fail); while let Some(cause) = cur_fail { err_msg += &format!("\n caused by: {}", cause); cur_fail = cause.source(); } error!("[{}] {}", get_test_id(), err_msg); panic!("{}", msg); } } } } pub fn issuer_url(test_id: &str) -> IssuerUrl { IssuerUrl::new(format!( "{}/{}/{}", CERTIFICATION_BASE_URL, RP_NAME, test_id )) .expect("Failed to parse issuer URL") } pub fn get_provider_metadata(test_id: &str) -> CoreProviderMetadata { let _issuer_url = issuer_url(test_id); CoreProviderMetadata::discover(&_issuer_url, &http_client).expect(&format!( "Failed to fetch provider metadata from {:?}", _issuer_url )) } pub fn register_client( provider_metadata: &CoreProviderMetadata, request_fn: F, ) -> CoreClientRegistrationResponse where F: FnOnce(CoreClientRegistrationRequest) -> CoreClientRegistrationRequest, { let registration_request_pre = CoreClientRegistrationRequest::new( vec![RedirectUrl::new(RP_REDIRECT_URI.to_string()).unwrap()], Default::default(), ) .set_application_type(Some(CoreApplicationType::Native)) .set_client_name(Some( vec![(None, ClientName::new(RP_NAME.to_string()))] .into_iter() .collect(), )) .set_contacts(Some(vec![ClientContactEmail::new( RP_CONTACT_EMAIL.to_string(), )])); let registration_request_post = request_fn(registration_request_pre); let registration_endpoint = provider_metadata .registration_endpoint() .expect("provider does not support dynamic registration"); registration_request_post .register(registration_endpoint, &http_client) .expect(&format!( "Failed to register client at {:?}", registration_endpoint )) }