Showing preview only (737K chars total). Download the full file or copy to clipboard to get everything.
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 <ramos@cs.stanford.edu>"]
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
[](https://crates.io/crates/openidconnect)
[](https://docs.rs/openidconnect)
[](https://github.com/ramosbugs/openidconnect-rs/actions/workflows/main.yml)
[](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<EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointSet, EndpointNotSet>`
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<CoreErrorResponseType>,
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<CoreErrorResponseType>,
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::<EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointNotSet>::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>,
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<Output = Result<HttpResponse, E>>,
```
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<HttpResponse, E>
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<String>,
groups: Vec<String>,
}
impl AdditionalClaims for GitLabClaims {}
fn handle_error<T: std::error::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::<CoreResponseType>::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<GitLabClaims, CoreGenderClaim> = 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<T: std::error::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<String, serde_json::Value>);
// 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::<CoreResponseType>::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<T: std::error::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<RT: ResponseType> {
/// 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<RT>),
}
/// 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<AuthenticationContextClass>,
pub(crate) authentication_flow: AuthenticationFlow<RT>,
pub(crate) claims_locales: Vec<LanguageTag>,
pub(crate) display: Option<AD>,
pub(crate) id_token_hint: Option<String>,
pub(crate) login_hint: Option<LoginHint>,
pub(crate) max_age: Option<Duration>,
pub(crate) nonce: Nonce,
pub(crate) prompts: Vec<P>,
pub(crate) ui_locales: Vec<LanguageTag>,
}
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<I>(mut self, scopes: I) -> Self
where
I: IntoIterator<Item = Scope>,
{
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<N, V>(mut self, name: N, value: V) -> Self
where
N: Into<Cow<'a, str>>,
V: Into<Cow<'a, str>>,
{
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<AC, GC, JE, JS>(
mut self,
id_token_hint: &'a IdToken<AC, GC, JE, JS>,
) -> Self
where
AC: AdditionalClaims,
GC: GenderClaim,
JE: JweContentEncryptionAlgorithm<KeyType = JS::KeyType>,
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<CoreResponseType>,
|| 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::<CoreResponseType>::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::<CoreIdToken>(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<FormattedAddress>,
/// 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<StreetAddress>,
/// City or locality component.
pub locality: Option<AddressLocality>,
/// State, province, prefecture, or region component.
pub region: Option<AddressRegion>,
/// Zip code or postal code component.
pub postal_code: Option<AddressPostalCode>,
/// Country name component.
pub country: Option<AddressCountry>,
}
/// 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<GC>
where
GC: GenderClaim,
{
pub(crate) sub: SubjectIdentifier,
pub(crate) name: Option<LocalizedClaim<EndUserName>>,
pub(crate) given_name: Option<LocalizedClaim<EndUserGivenName>>,
pub(crate) family_name: Option<LocalizedClaim<EndUserFamilyName>>,
pub(crate) middle_name: Option<LocalizedClaim<EndUserMiddleName>>,
pub(crate) nickname: Option<LocalizedClaim<EndUserNickname>>,
pub(crate) preferred_username: Option<EndUserUsername>,
pub(crate) profile: Option<LocalizedClaim<EndUserProfileUrl>>,
pub(crate) picture: Option<LocalizedClaim<EndUserPictureUrl>>,
pub(crate) website: Option<LocalizedClaim<EndUserWebsiteUrl>>,
pub(crate) email: Option<EndUserEmail>,
pub(crate) email_verified: Option<bool>,
pub(crate) gender: Option<GC>,
pub(crate) birthday: Option<EndUserBirthday>,
pub(crate) birthdate: Option<EndUserBirthday>,
pub(crate) zoneinfo: Option<EndUserTimezone>,
pub(crate) locale: Option<LanguageTag>,
pub(crate) phone_number: Option<EndUserPhoneNumber>,
pub(crate) phone_number_verified: Option<bool>,
pub(crate) address: Option<AddressClaim>,
pub(crate) updated_at: Option<DateTime<Utc>>,
}
impl<GC> StandardClaims<GC>
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<LocalizedClaim<EndUserName>>],
set_given_name -> given_name[Option<LocalizedClaim<EndUserGivenName>>],
set_family_name ->
family_name[Option<LocalizedClaim<EndUserFamilyName>>],
set_middle_name ->
middle_name[Option<LocalizedClaim<EndUserMiddleName>>],
set_nickname -> nickname[Option<LocalizedClaim<EndUserNickname>>],
set_preferred_username -> preferred_username[Option<EndUserUsername>],
set_profile -> profile[Option<LocalizedClaim<EndUserProfileUrl>>],
set_picture -> picture[Option<LocalizedClaim<EndUserPictureUrl>>],
set_website -> website[Option<LocalizedClaim<EndUserWebsiteUrl>>],
set_email -> email[Option<EndUserEmail>],
set_email_verified -> email_verified[Option<bool>],
set_gender -> gender[Option<GC>],
set_birthday -> birthday[Option<EndUserBirthday>],
set_birthdate -> birthdate[Option<EndUserBirthday>],
set_zoneinfo -> zoneinfo[Option<EndUserTimezone>],
set_locale -> locale[Option<LanguageTag>],
set_phone_number -> phone_number[Option<EndUserPhoneNumber>],
set_phone_number_verified -> phone_number_verified[Option<bool>],
set_address -> address[Option<AddressClaim>],
set_updated_at -> updated_at[Option<DateTime<Utc>>],
}
];
}
impl<GC> FlattenFilter for StandardClaims<GC>
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<GC>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ClaimsVisitor<GC: GenderClaim>(PhantomData<GC>);
impl<'de, GC> Visitor<'de> for ClaimsVisitor<GC>
where
GC: GenderClaim,
{
type Value = StandardClaims<GC>;
fn expecting(&self, formatter: &mut Formatter) -> FormatterResult {
formatter.write_str("struct StandardClaims")
}
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
// NB: The non-localized fields are actually Option<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::<String>()? {
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<Option<_>>, language_tag),
"given_name" => {
field_case!(given_name, LocalizedClaim<Option<_>>, language_tag)
}
"family_name" => {
field_case!(family_name, LocalizedClaim<Option<_>>, language_tag)
}
"middle_name" => {
field_case!(middle_name, LocalizedClaim<Option<_>>, language_tag)
}
"nickname" => {
field_case!(nickname, LocalizedClaim<Option<_>>, language_tag)
}
"preferred_username" => {
field_case!(preferred_username, Option<_>, language_tag)
}
"profile" => {
field_case!(profile, LocalizedClaim<Option<_>>, language_tag)
}
"picture" => {
field_case!(picture, LocalizedClaim<Option<_>>, language_tag)
}
"website" => {
field_case!(website, LocalizedClaim<Option<_>>, language_tag)
}
"email" => field_case!(email, Option<_>, language_tag),
"email_verified" => {
field_case!(email_verified, Option<Boolean>, 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<Boolean>, language_tag)
}
"address" => field_case!(address, Option<_>, language_tag),
"updated_at" => field_case!(updated_at, Option<Timestamp>, language_tag),
// Ignore unknown fields.
_ => {
map.next_value::<serde::de::IgnoredAny>()?;
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<GC> Serialize for StandardClaims<GC>
where
GC: GenderClaim,
{
#[allow(clippy::cognitive_complexity)]
fn serialize<SE>(&self, serializer: SE) -> Result<SE::Ok, SE::Error>
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::<StandardClaims<CoreGenderClaim>>(
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<StandardClaims<CoreGenderClaim>, 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<HttpResponse, FakeError> {
/// # 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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<ClientSecret>,
pub(crate) issuer: IssuerUrl,
userinfo_endpoint: Option<UserInfoUrl>,
pub(crate) jwks: JsonWebKeySet<K>,
id_token_signing_algs: Option<Vec<K::SigningAlgorithm>>,
use_openid_scope: bool,
_phantom: PhantomData<(AC, AD, GC, JE, P, HasUserInfoUrl)>,
}
impl<AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE>
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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
TIR: TokenIntrospectionResponse,
RT: RevocableToken,
TRE: ErrorResponse + 'static,
{
/// Initialize an OpenID Connect client.
pub fn new(client_id: ClientId, issuer: IssuerUrl, jwks: JsonWebKeySet<K>) -> 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<AC, AD, GC, JE, K, P, TE, TR, TIR, RT, TRE>
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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<A, CA, CN, CT, G, JK, RM, RS, S>(
provider_metadata: ProviderMetadata<A, AD, CA, CN, CT, G, JE, JK, K, RM, RS, S>,
client_id: ClientId,
client_secret: Option<ClientSecret>,
) -> 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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<K> {
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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<NF, RS, SF>(
&self,
authentication_flow: AuthenticationFlow<RS>,
state_fn: SF,
nonce_fn: NF,
) -> AuthorizationRequest<AD, P, RS>
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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<TE, TR> {
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<TE, TR> {
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<EF>,
) -> 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 <https://tools.ietf.org/html/rfc6749#section-6>.
///
/// 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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<ClientCredentialsTokenRequest<TE, TR>, 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<CodeTokenRequest<TE, TR>, 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<EF>,
) -> Result<DeviceAccessTokenRequest<'a, 'static, TR, EF>, 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<PasswordTokenRequest<'a, TE, TR>, ConfigurationError> {
self.oauth2_client.exchange_password(username, password)
}
/// Exchange a refresh token for an access token.
///
/// See <https://tools.ietf.org/html/rfc6749#section-6>.
///
/// 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<RefreshTokenRequest<'a, TE, TR>, 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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<TE> {
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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<RevocationRequest<RT, TRE>, 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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<SubjectIdentifier>,
) -> UserInfoRequest<JE, K> {
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 = <K::SigningAlgorithm as JwsSigningAlgorithm>::KeyType,
>,
K: JsonWebKey,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, K::SigningAlgorithm>,
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<SubjectIdentifier>,
) -> Result<UserInfoRequest<JE, K>, 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::<sha2::Sha256>(),
&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<CoreJsonWebKeyUse>,
pub(crate) kid: Option<JsonWebKeyId>,
/// 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<JsonWebTokenAlgorithm<CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm>>,
// 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<Base64UrlEncodedBytes>,
#[serde(default, deserialize_with = "deserialize_option_or_none")]
pub(crate) e: Option<Base64UrlEncodedBytes>,
//Elliptic Curve
#[serde(default, deserialize_with = "deserialize_option_or_none")]
pub(crate) crv: Option<CoreJsonCurveType>,
#[serde(default, deserialize_with = "deserialize_option_or_none")]
pub(crate) x: Option<Base64UrlEncodedBytes>,
#[serde(default, deserialize_with = "deserialize_option_or_none")]
pub(crate) y: Option<Base64UrlEncodedBytes>,
#[serde(default, deserialize_with = "deserialize_option_or_none")]
pub(crate) d: Option<Base64UrlEncodedBytes>,
// 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<Base64UrlEncodedBytes>,
}
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<u8>, e: Vec<u8>, kid: Option<JsonWebKeyId>) -> 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<u8>,
y: Vec<u8>,
crv: CoreJsonCurveType,
kid: Option<JsonWebKeyId>,
) -> 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<u8>, crv: CoreJsonCurveType, kid: Option<JsonWebKeyId>) -> 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<u8>) -> 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::<sha2::Sha256>(),
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::<sha2::Sha384>(),
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::<sha2::Sha512>(),
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::<sha2::Sha256>(),
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::<sha2::Sha384>(),
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::<sha2::Sha512>(),
message,
signature,
)
}
CoreJwsSigningAlgorithm::HmacSha256 => {
let mut mac = hmac::Hmac::<sha2::Sha256>::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::<sha2::Sha384>::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::<sha2::Sha512>::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<Vec<u8>, 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<u8>,
}
impl CoreHmacKey {
/// Instantiate a new key from the specified secret bytes.
pub fn new<T>(secret: T) -> Self
where
T: Into<Vec<u8>>,
{
Self {
secret: secret.into(),
}
}
}
impl PrivateSigningKey for CoreHmacKey {
type VerificationKey = CoreJsonWebKey;
fn sign(
&self,
signature_alg: &CoreJwsSigningAlgorithm,
message: &[u8],
) -> Result<Vec<u8>, SigningError> {
use hmac::Mac;
match *signature_alg {
CoreJwsSigningAlgorithm::HmacSha256 => {
let mut mac = hmac::Hmac::<sha2::Sha256>::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::<sha2::Sha384>::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::<sha2::Sha512>::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<Self, String> {
Ok(Self::Ed25519(
ed25519_dalek::SigningKey::from_pkcs8_pem(pem).map_err(|err| err.to_string())?,
))
}
fn sign(&self, message: &[u8]) -> Vec<u8> {
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<JsonWebKeyId>,
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<JsonWebKeyId>) -> Result<Self, String> {
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<Vec<u8>, 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<T> 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<dyn RngClone + Send + Sync>,
kid: Option<JsonWebKeyId>,
}
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<JsonWebKeyId>) -> Result<Self, String> {
Self::from_pem_internal(pem, Box::new(rand::rngs::OsRng), kid)
}
pub(crate) fn from_pem_internal(
pem: &str,
rng: Box<dyn RngClone + Send + Sync>,
kid: Option<JsonWebKeyId>,
) -> Result<Self, String> {
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<Vec<u8>, 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::<sha2::Sha256>(),
&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::<sha2::Sha384>(),
&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::<sha2::Sha512>(),
&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::<sha2::Sha256>(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::<sha2::Sha384>(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::<sha2::Sha512>(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<str> 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::<CoreJsonWebKey>(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_si
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
SYMBOL INDEX (662 symbols across 34 files)
FILE: examples/gitlab.rs
type GitLabClaims (line 38) | struct GitLabClaims {
function handle_error (line 45) | fn handle_error<T: std::error::Error>(fail: &T, msg: &'static str) {
function main (line 56) | fn main() {
FILE: examples/google.rs
function handle_error (line 37) | fn handle_error<T: std::error::Error>(fail: &T, msg: &'static str) {
type RevocationEndpointProviderMetadata (line 52) | struct RevocationEndpointProviderMetadata {
type GoogleProviderMetadata (line 56) | type GoogleProviderMetadata = ProviderMetadata<
function main (line 71) | fn main() {
FILE: examples/okta_device_grant.rs
type DeviceEndpointProviderMetadata (line 36) | struct DeviceEndpointProviderMetadata {
type DeviceProviderMetadata (line 40) | type DeviceProviderMetadata = ProviderMetadata<
function handle_error (line 55) | fn handle_error<T: std::error::Error>(fail: &T, msg: &'static str) {
function main (line 66) | fn main() -> Result<(), anyhow::Error> {
FILE: src/authorization.rs
type AuthenticationFlow (line 18) | pub enum AuthenticationFlow<RT: ResponseType> {
type AuthorizationRequest (line 42) | pub struct AuthorizationRequest<'a, AD, P, RT>
function add_scope (line 67) | pub fn add_scope(mut self, scope: Scope) -> Self {
function add_scopes (line 73) | pub fn add_scopes<I>(mut self, scopes: I) -> Self
function add_extra_param (line 94) | pub fn add_extra_param<N, V>(mut self, name: N, value: V) -> Self
function set_pkce_challenge (line 109) | pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallen...
function add_auth_context_value (line 119) | pub fn add_auth_context_value(mut self, acr_value: AuthenticationContext...
function add_claims_locale (line 127) | pub fn add_claims_locale(mut self, claims_locale: LanguageTag) -> Self {
function set_display (line 137) | pub fn set_display(mut self, display: AD) -> Self {
function set_id_token_hint (line 149) | pub fn set_id_token_hint<AC, GC, JE, JS>(
function set_login_hint (line 166) | pub fn set_login_hint(mut self, login_hint: LoginHint) -> Self {
function set_max_age (line 175) | pub fn set_max_age(mut self, max_age: Duration) -> Self {
function add_prompt (line 182) | pub fn add_prompt(mut self, prompt: P) -> Self {
function add_ui_locale (line 191) | pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self {
function set_redirect_uri (line 197) | pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) ->...
function url (line 204) | pub fn url(self) -> (Url, CsrfToken, Nonce) {
function new_client (line 278) | fn new_client() -> CoreClient<
function test_authorize_url_minimal (line 298) | fn test_authorize_url_minimal() {
function test_authorize_url_implicit_with_access_token (line 317) | fn test_authorize_url_implicit_with_access_token() {
function test_authorize_url_hybrid (line 336) | fn test_authorize_url_hybrid() {
function test_authorize_url_full (line 358) | fn test_authorize_url_full() {
function test_authorize_url_redirect_url_override (line 469) | fn test_authorize_url_redirect_url_override() {
FILE: src/claims.rs
type AdditionalClaims (line 22) | pub trait AdditionalClaims: Debug + DeserializeOwned + Serialize + 'stat...
type EmptyAdditionalClaims (line 28) | pub struct EmptyAdditionalClaims {}
type AddressClaim (line 34) | pub struct AddressClaim {
type GenderClaim (line 59) | pub trait GenderClaim: Clone + Debug + DeserializeOwned + Serialize + 's...
type StandardClaims (line 63) | pub struct StandardClaims<GC>
function new (line 96) | pub fn new(subject: SubjectIdentifier) -> Self {
function subject (line 123) | pub fn subject(&self) -> &SubjectIdentifier {
function set_subject (line 128) | pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self {
method should_include (line 166) | fn should_include(field_name: &str) -> bool {
function deserialize (line 199) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
method serialize (line 349) | fn serialize<SE>(&self, serializer: SE) -> Result<SE::Ok, SE::Error>
function test_null_optional_claims (line 391) | fn test_null_optional_claims() {
function expect_err_prefix (line 423) | fn expect_err_prefix(
function test_duplicate_claims (line 437) | fn test_duplicate_claims() {
function test_err_field_name (line 472) | fn test_err_field_name() {
FILE: src/client.rs
constant OPENID_SCOPE (line 18) | const OPENID_SCOPE: &str = "openid";
type Client (line 93) | pub struct Client<
function new (line 189) | pub fn new(client_id: ClientId, issuer: IssuerUrl, jwks: JsonWebKeySet<K...
function from_provider_metadata (line 242) | pub fn from_provider_metadata<A, CA, CN, CT, G, JK, RM, RS, S>(
function set_auth_type (line 349) | pub fn set_auth_type(mut self, auth_type: AuthType) -> Self {
function auth_type (line 356) | pub fn auth_type(&self) -> &AuthType {
function set_auth_uri (line 366) | pub fn set_auth_uri(
function client_id (line 402) | pub fn client_id(&self) -> &ClientId {
function set_client_secret (line 411) | pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self {
function set_device_authorization_url (line 422) | pub fn set_device_authorization_url(
function set_introspection_url (line 462) | pub fn set_introspection_url(
function set_redirect_uri (line 498) | pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self {
function redirect_uri (line 504) | pub fn redirect_uri(&self) -> Option<&RedirectUrl> {
function set_revocation_url (line 511) | pub fn set_revocation_url(
function set_token_uri (line 552) | pub fn set_token_uri(
function set_user_info_url (line 590) | pub fn set_user_info_url(
function enable_openid_scope (line 629) | pub fn enable_openid_scope(mut self) -> Self {
function disable_openid_scope (line 635) | pub fn disable_openid_scope(mut self) -> Self {
function id_token_verifier (line 642) | pub fn id_token_verifier(&self) -> IdTokenVerifier<K> {
function auth_uri (line 725) | pub fn auth_uri(&self) -> &AuthUrl {
function authorize_url (line 760) | pub fn authorize_url<NF, RS, SF>(
function exchange_client_credentials (line 855) | pub fn exchange_client_credentials(&self) -> ClientCredentialsTokenReque...
function exchange_code (line 868) | pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest...
function exchange_device_access_token (line 878) | pub fn exchange_device_access_token<'a, EF>(
function exchange_password (line 895) | pub fn exchange_password<'a>(
function exchange_refresh_token (line 910) | pub fn exchange_refresh_token<'a>(
function token_uri (line 918) | pub fn token_uri(&self) -> &TokenUrl {
function exchange_client_credentials (line 987) | pub fn exchange_client_credentials(
function exchange_code (line 1002) | pub fn exchange_code(
function exchange_device_access_token (line 1015) | pub fn exchange_device_access_token<'a, EF>(
function exchange_password (line 1031) | pub fn exchange_password<'a>(
function exchange_refresh_token (line 1045) | pub fn exchange_refresh_token<'a>(
function token_uri (line 1053) | pub fn token_uri(&self) -> Option<&TokenUrl> {
function exchange_device_code (line 1124) | pub fn exchange_device_code(&self) -> DeviceAuthorizationRequest<TE> {
function device_authorization_url (line 1137) | pub fn device_authorization_url(&self) -> &DeviceAuthorizationUrl {
function introspect (line 1205) | pub fn introspect<'a>(&'a self, token: &'a AccessToken) -> Introspection...
function introspection_url (line 1210) | pub fn introspection_url(&self) -> &IntrospectionUrl {
function revoke_token (line 1278) | pub fn revoke_token(
function revocation_url (line 1288) | pub fn revocation_url(&self) -> &RevocationUrl {
function user_info (line 1362) | pub fn user_info(
function user_info_url (line 1373) | pub fn user_info_url(&self) -> &UserInfoUrl {
function user_info (line 1450) | pub fn user_info(
function user_info_url (line 1467) | pub fn user_info_url(&self) -> Option<&UserInfoUrl> {
FILE: src/core/crypto.rs
function rsa_public_key (line 8) | fn rsa_public_key(
function ec_public_key (line 26) | fn ec_public_key(
function ed_public_key (line 55) | fn ed_public_key(
function verify_rsa_signature (line 73) | pub fn verify_rsa_signature(
function verify_ec_signature (line 100) | pub fn verify_ec_signature(
function verify_ed_signature (line 149) | pub fn verify_ed_signature(
function test_leading_zeros_are_parsed_correctly (line 190) | fn test_leading_zeros_are_parsed_correctly() {
FILE: src/core/jwk/mod.rs
type CoreJsonWebKey (line 29) | pub struct CoreJsonWebKey {
method new_rsa (line 73) | pub fn new_rsa(n: Vec<u8>, e: Vec<u8>, kid: Option<JsonWebKeyId>) -> S...
method new_ec (line 93) | pub fn new_ec(
method new_okp (line 119) | pub fn new_okp(x: Vec<u8>, crv: CoreJsonCurveType, kid: Option<JsonWeb...
type KeyUse (line 136) | type KeyUse = CoreJsonWebKeyUse;
type SigningAlgorithm (line 137) | type SigningAlgorithm = CoreJwsSigningAlgorithm;
method key_id (line 139) | fn key_id(&self) -> Option<&JsonWebKeyId> {
method key_type (line 142) | fn key_type(&self) -> &CoreJsonWebKeyType {
method key_use (line 145) | fn key_use(&self) -> Option<&CoreJsonWebKeyUse> {
method signing_alg (line 149) | fn signing_alg(&self) -> JsonWebKeyAlgorithm<&CoreJwsSigningAlgorithm> {
method new_symmetric (line 157) | fn new_symmetric(key: Vec<u8>) -> Self {
method verify_signature (line 173) | fn verify_signature(
method hash_bytes (line 348) | fn hash_bytes(&self, bytes: &[u8], alg: &Self::SigningAlgorithm) -> Resu...
type CoreHmacKey (line 397) | pub struct CoreHmacKey {
method new (line 402) | pub fn new<T>(secret: T) -> Self
type VerificationKey (line 412) | type VerificationKey = CoreJsonWebKey;
method sign (line 414) | fn sign(
method as_verification_key (line 453) | fn as_verification_key(&self) -> CoreJsonWebKey {
type EdDsaSigningKey (line 458) | enum EdDsaSigningKey {
method from_ed25519_pem (line 463) | fn from_ed25519_pem(pem: &str) -> Result<Self, String> {
method sign (line 469) | fn sign(&self, message: &[u8]) -> Vec<u8> {
type CoreEdDsaPrivateSigningKey (line 484) | pub struct CoreEdDsaPrivateSigningKey {
method from_ed25519_pem (line 490) | pub fn from_ed25519_pem(pem: &str, kid: Option<JsonWebKeyId>) -> Resul...
type VerificationKey (line 498) | type VerificationKey = CoreJsonWebKey;
method sign (line 500) | fn sign(
method as_verification_key (line 518) | fn as_verification_key(&self) -> CoreJsonWebKey {
type RngClone (line 541) | pub(crate) trait RngClone: dyn_clone::DynClone + rand::RngCore + rand::C...
type CoreRsaPrivateSigningKey (line 549) | pub struct CoreRsaPrivateSigningKey {
method from_pem (line 556) | pub fn from_pem(pem: &str, kid: Option<JsonWebKeyId>) -> Result<Self, ...
method from_pem_internal (line 560) | pub(crate) fn from_pem_internal(
type VerificationKey (line 570) | type VerificationKey = CoreJsonWebKey;
method sign (line 572) | fn sign(
method as_verification_key (line 667) | fn as_verification_key(&self) -> CoreJsonWebKey {
type CoreJsonWebKeyType (line 690) | pub enum CoreJsonWebKeyType {
type CoreJsonCurveType (line 711) | pub enum CoreJsonCurveType {
type CoreJsonWebKeyUse (line 729) | pub enum CoreJsonWebKeyUse {
method from_str (line 740) | fn from_str(s: &str) -> Self {
method as_ref (line 749) | fn as_ref(&self) -> &str {
method allows_signature (line 758) | fn allows_signature(&self) -> bool {
method allows_encryption (line 761) | fn allows_encryption(&self) -> bool {
FILE: src/core/jwk/tests.rs
function test_core_jwk_deserialization_rsa (line 21) | fn test_core_jwk_deserialization_rsa() {
function test_core_jwk_deserialization_ec (line 61) | fn test_core_jwk_deserialization_ec() {
function test_core_jwk_deserialization_ed (line 95) | fn test_core_jwk_deserialization_ed() {
function test_core_jwk_deserialization_symmetric (line 119) | fn test_core_jwk_deserialization_symmetric() {
function test_core_jwk_deserialization_no_optional (line 147) | fn test_core_jwk_deserialization_no_optional() {
function test_core_jwk_deserialization_unrecognized (line 159) | fn test_core_jwk_deserialization_unrecognized() {
function test_core_jwk_deserialization_dupe_fields (line 170) | fn test_core_jwk_deserialization_dupe_fields() {
function verify_signature (line 191) | fn verify_signature(
function verify_invalid_signature (line 215) | fn verify_invalid_signature(
function test_eddsa_verification (line 234) | fn test_eddsa_verification() {
function test_ecdsa_verification (line 291) | fn test_ecdsa_verification() {
function test_rsa_pkcs1_verification (line 379) | fn test_rsa_pkcs1_verification() {
function test_rsa_pss_verification (line 497) | fn test_rsa_pss_verification() {
function test_hmac_sha256_verification (line 546) | fn test_hmac_sha256_verification() {
function expect_hmac (line 587) | fn expect_hmac(
function test_hmac_signing (line 603) | fn test_hmac_signing() {
constant TEST_ED25519_KEY (line 670) | const TEST_ED25519_KEY: &str = "\
constant TEST_RSA_KEY (line 677) | const TEST_RSA_KEY: &str = "\
function expect_ed_sig (line 707) | fn expect_ed_sig(
function expect_rsa_sig (line 720) | fn expect_rsa_sig(
type TestRng (line 734) | struct TestRng(StepRng);
method next_u32 (line 738) | fn next_u32(&mut self) -> u32 {
method next_u64 (line 741) | fn next_u64(&mut self) -> u64 {
method fill_bytes (line 744) | fn fill_bytes(&mut self, dest: &mut [u8]) {
method try_fill_bytes (line 747) | fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::E...
function test_ed_signing (line 753) | fn test_ed_signing() {
function test_rsa_signing (line 794) | fn test_rsa_signing() {
function test_rsa_pss_signing (line 912) | fn test_rsa_pss_signing() {
function test_jwks_unsupported_key (line 936) | fn test_jwks_unsupported_key() {
function test_jwks_unsupported_alg (line 989) | fn test_jwks_unsupported_alg() {
function test_jwks_same_kid_different_alg (line 1018) | fn test_jwks_same_kid_different_alg() {
function test_hash_bytes_eddsa (line 1075) | fn test_hash_bytes_eddsa() {
function test_hash_bytes_rsa (line 1097) | fn test_hash_bytes_rsa() {
FILE: src/core/mod.rs
type CoreDeviceAuthorizationResponse (line 43) | pub type CoreDeviceAuthorizationResponse =
type CoreTokenIntrospectionResponse (line 47) | pub type CoreTokenIntrospectionResponse =
type CoreAuthenticationFlow (line 51) | pub type CoreAuthenticationFlow = AuthenticationFlow<CoreResponseType>;
type CoreClient (line 54) | pub type CoreClient<
type CoreClientMetadata (line 82) | pub type CoreClientMetadata = ClientMetadata<
type CoreClientRegistrationRequest (line 95) | pub type CoreClientRegistrationRequest = ClientRegistrationRequest<
type CoreClientRegistrationResponse (line 110) | pub type CoreClientRegistrationResponse = ClientRegistrationResponse<
type CoreIdToken (line 124) | pub type CoreIdToken = IdToken<
type CoreIdTokenClaims (line 132) | pub type CoreIdTokenClaims = IdTokenClaims<EmptyAdditionalClaims, CoreGe...
type CoreIdTokenFields (line 135) | pub type CoreIdTokenFields = IdTokenFields<
type CoreIdTokenVerifier (line 144) | pub type CoreIdTokenVerifier<'a> = IdTokenVerifier<'a, CoreJsonWebKey>;
type CoreTokenResponse (line 147) | pub type CoreTokenResponse = StandardTokenResponse<CoreIdTokenFields, Co...
type CoreJsonWebKeySet (line 150) | pub type CoreJsonWebKeySet = JsonWebKeySet<CoreJsonWebKey>;
type CoreProviderMetadata (line 153) | pub type CoreProviderMetadata = ProviderMetadata<
type CoreUserInfoClaims (line 169) | pub type CoreUserInfoClaims = UserInfoClaims<EmptyAdditionalClaims, Core...
type CoreUserInfoJsonWebToken (line 172) | pub type CoreUserInfoJsonWebToken = UserInfoJsonWebToken<
type CoreUserInfoVerifier (line 180) | pub type CoreUserInfoVerifier<'a> =
type CoreApplicationType (line 189) | pub enum CoreApplicationType {
method from_str (line 206) | fn from_str(s: &str) -> Self {
method as_ref (line 215) | fn as_ref(&self) -> &str {
type CoreAuthDisplay (line 231) | pub enum CoreAuthDisplay {
method from_str (line 253) | fn from_str(s: &str) -> Self {
method as_ref (line 264) | fn as_ref(&self) -> &str {
method fmt (line 276) | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
type CoreAuthPrompt (line 287) | pub enum CoreAuthPrompt {
method from_str (line 315) | fn from_str(s: &str) -> Self {
method as_ref (line 326) | fn as_ref(&self) -> &str {
method fmt (line 339) | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
type CoreClaimType (line 356) | pub enum CoreClaimType {
method from_str (line 378) | fn from_str(s: &str) -> Self {
method as_ref (line 388) | fn as_ref(&self) -> &str {
type CoreClientAuthMethod (line 401) | pub enum CoreClientAuthMethod {
method from_str (line 422) | fn from_str(s: &str) -> Self {
method as_ref (line 434) | fn as_ref(&self) -> &str {
type CoreGrantType (line 459) | pub enum CoreGrantType {
method from_str (line 480) | fn from_str(s: &str) -> Self {
method as_ref (line 494) | fn as_ref(&self) -> &str {
type CoreJweContentEncryptionAlgorithm (line 516) | pub enum CoreJweContentEncryptionAlgorithm {
type KeyType (line 537) | type KeyType = CoreJsonWebKeyType;
method key_type (line 539) | fn key_type(&self) -> Result<CoreJsonWebKeyType, String> {
type CoreJweKeyManagementAlgorithm (line 552) | pub enum CoreJweKeyManagementAlgorithm {
type CoreJwsSigningAlgorithm (line 615) | pub enum CoreJwsSigningAlgorithm {
type KeyType (line 668) | type KeyType = CoreJsonWebKeyType;
method key_type (line 670) | fn key_type(&self) -> Option<CoreJsonWebKeyType> {
method uses_shared_secret (line 689) | fn uses_shared_secret(&self) -> bool {
method rsa_sha_256 (line 695) | fn rsa_sha_256() -> Self {
type CoreAuthErrorResponseType (line 710) | pub enum CoreAuthErrorResponseType {
method from_str (line 765) | fn from_str(s: &str) -> Self {
method as_ref (line 788) | fn as_ref(&self) -> &str {
type CoreRegisterErrorResponseType (line 813) | pub enum CoreRegisterErrorResponseType {
method from_str (line 826) | fn from_str(s: &str) -> Self {
method as_ref (line 835) | fn as_ref(&self) -> &str {
method fmt (line 846) | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
type CoreResponseMode (line 864) | pub enum CoreResponseMode {
method from_str (line 895) | fn from_str(s: &str) -> Self {
method as_ref (line 905) | fn as_ref(&self) -> &str {
type CoreResponseType (line 924) | pub enum CoreResponseType {
method from_str (line 951) | fn from_str(s: &str) -> Self {
method as_ref (line 962) | fn as_ref(&self) -> &str {
method to_oauth2 (line 973) | fn to_oauth2(&self) -> OAuth2ResponseType {
type CoreSubjectIdentifierType (line 986) | pub enum CoreSubjectIdentifierType {
method from_str (line 999) | fn from_str(s: &str) -> Self {
method as_ref (line 1008) | fn as_ref(&self) -> &str {
function base64_url_safe_no_pad (line 1018) | pub(crate) fn base64_url_safe_no_pad() -> GeneralPurpose {
FILE: src/core/tests.rs
function test_grant_type_serialize (line 4) | fn test_grant_type_serialize() {
function test_signature_alg_serde_plain (line 14) | fn test_signature_alg_serde_plain() {
FILE: src/discovery/mod.rs
constant CONFIG_URL_SUFFIX (line 25) | const CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration";
type AdditionalProviderMetadata (line 28) | pub trait AdditionalProviderMetadata: Clone + Debug + DeserializeOwned +...
type EmptyAdditionalProviderMetadata (line 34) | pub struct EmptyAdditionalProviderMetadata {}
type ProviderMetadata (line 43) | pub struct ProviderMetadata<A, AD, CA, CN, CT, G, JE, JK, K, RM, RT, S>
function new (line 172) | pub fn new(
function discover (line 277) | pub fn discover<C>(
function discover_async (line 308) | pub fn discover_async<'c, C>(
function discovery_request (line 342) | fn discovery_request(discovery_url: url::Url) -> Result<HttpRequest, htt...
function discovery_response (line 350) | fn discovery_response<RE>(
function additional_metadata (line 395) | pub fn additional_metadata(&self) -> &A {
function additional_metadata_mut (line 399) | pub fn additional_metadata_mut(&mut self) -> &mut A {
type DiscoveryError (line 407) | pub enum DiscoveryError<RE>
FILE: src/discovery/tests.rs
function test_discovery_deserialization (line 12) | fn test_discovery_deserialization() {
function test_discovery_deserialization_other_fields (line 743) | fn test_discovery_deserialization_other_fields() {
function test_unsupported_enum_values (line 976) | fn test_unsupported_enum_values() {
FILE: src/helpers.rs
function deserialize_string_or_vec (line 16) | pub(crate) fn deserialize_string_or_vec<'de, T, D>(deserializer: D) -> R...
function deserialize_string_or_vec_opt (line 31) | pub(crate) fn deserialize_string_or_vec_opt<'de, T, D>(
function deserialize_option_or_none (line 53) | pub(crate) fn deserialize_option_or_none<'de, T, D>(deserializer: D) -> ...
type DeserializeMapField (line 65) | pub trait DeserializeMapField: Sized {
method deserialize_map_field (line 66) | fn deserialize_map_field<'de, V>(
method deserialize_map_field (line 80) | fn deserialize_map_field<'de, V>(
method deserialize_map_field (line 109) | fn deserialize_map_field<'de, V>(
function deserialize (line 146) | pub fn deserialize<'de, D>(deserializer: D) -> Result<bool, D::Error>
function serialize_space_delimited_vec (line 182) | pub(crate) fn serialize_space_delimited_vec<T, S>(
type FlattenFilter (line 199) | pub(crate) trait FlattenFilter {
method should_include (line 200) | fn should_include(field_name: &str) -> bool;
type FilteredFlatten (line 209) | pub(crate) struct FilteredFlatten<F, T>
function from (line 227) | fn from(value: T) -> Self {
function as_ref (line 239) | fn as_ref(&self) -> &T {
function as_mut (line 248) | fn as_mut(&mut self) -> &mut T {
method eq (line 257) | fn eq(&self, other: &Self) -> bool {
method clone (line 266) | fn clone(&self) -> Self {
method fmt (line 281) | fn fmt(&self, f: &mut Formatter) -> FormatterResult {
function deserialize (line 291) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
type Flatten (line 334) | struct Flatten<T>
function as_ref (line 345) | fn as_ref(&self) -> &T {
function as_mut (line 353) | fn as_mut(&mut self) -> &mut T {
method eq (line 361) | fn eq(&self, other: &Self) -> bool {
method fmt (line 370) | fn fmt(&self, f: &mut Formatter) -> FormatterResult {
function join_vec (line 375) | pub(crate) fn join_vec<T>(entries: &[T]) -> String
type Boolean (line 389) | pub(crate) struct Boolean(
method into_inner (line 397) | pub(crate) fn into_inner(self) -> bool {
method fmt (line 402) | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
type Timestamp (line 410) | pub(crate) enum Timestamp {
method from_utc (line 419) | pub(crate) fn from_utc(utc: &DateTime<Utc>) -> Self {
method to_utc (line 423) | pub(crate) fn to_utc(&self) -> Result<DateTime<Utc>, ()> {
method deserialize_as (line 458) | fn deserialize_as<D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
method serialize_as (line 473) | fn serialize_as<S>(source: &DateTime<Utc>, serializer: S) -> Result<S:...
method fmt (line 448) | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
function deserialize (line 498) | pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
function serialize (line 515) | pub fn serialize<S>(v: &[u8], serializer: S) -> Result<S::Ok, S::Error>
FILE: src/http_utils.rs
constant MIME_TYPE_JSON (line 5) | pub const MIME_TYPE_JSON: &str = "application/json";
constant MIME_TYPE_JWKS (line 6) | pub const MIME_TYPE_JWKS: &str = "application/jwk-set+json";
constant MIME_TYPE_JWT (line 7) | pub const MIME_TYPE_JWT: &str = "application/jwt";
constant BEARER (line 9) | pub const BEARER: &str = "Bearer";
function content_type_has_essence (line 13) | pub fn content_type_has_essence(content_type: &HeaderValue, expected_ess...
function check_content_type (line 24) | pub fn check_content_type(headers: &HeaderMap, expected_content_type: &s...
function auth_bearer (line 45) | pub fn auth_bearer(access_token: &AccessToken) -> (HeaderName, HeaderVal...
FILE: src/id_token/mod.rs
type IdToken (line 37) | pub struct IdToken<
type Err (line 54) | type Err = serde_json::Error;
method from_str (line 55) | fn from_str(s: &str) -> Result<Self, Self::Err> {
function new (line 73) | pub fn new<S>(
function claims (line 113) | pub fn claims<'a, K, N>(
function into_claims (line 126) | pub fn into_claims<K, N>(
function signing_alg (line 142) | pub fn signing_alg(&self) -> Result<&JS, SignatureVerificationError> {
function signing_key (line 163) | pub fn signing_key<'s, K>(
method to_string (line 182) | fn to_string(&self) -> String {
type IdTokenClaims (line 202) | pub struct IdTokenClaims<AC, GC>
function new (line 252) | pub fn new(
function subject (line 294) | pub fn subject(&self) -> &SubjectIdentifier {
function set_subject (line 298) | pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self {
function additional_claims (line 331) | pub fn additional_claims(&self) -> &AC {
function additional_claims_mut (line 335) | pub fn additional_claims_mut(&mut self) -> &mut AC {
method audiences (line 344) | fn audiences(&self) -> Option<&Vec<Audience>> {
method audiences (line 353) | fn audiences(&self) -> Option<&Vec<Audience>> {
method issuer (line 362) | fn issuer(&self) -> Option<&IssuerUrl> {
method issuer (line 371) | fn issuer(&self) -> Option<&IssuerUrl> {
type IdTokenFields (line 382) | pub struct IdTokenFields<AC, EF, GC, JE, JS>
function new (line 404) | pub fn new(id_token: Option<IdToken<AC, GC, JE, JS>>, extra_fields: EF) ...
function id_token (line 412) | pub fn id_token(&self) -> Option<&IdToken<AC, GC, JE, JS>> {
function extra_fields (line 416) | pub fn extra_fields(&self) -> &EF {
FILE: src/id_token/tests.rs
function test_id_token (line 26) | fn test_id_token() {
function test_oauth2_response (line 74) | fn test_oauth2_response() {
function test_minimal_claims_serde (line 123) | fn test_minimal_claims_serde() {
function test_complete_claims_serde (line 202) | fn test_complete_claims_serde() {
function test_accept_rfc3339_timestamp (line 471) | fn test_accept_rfc3339_timestamp() {
function test_unknown_claims_serde (line 494) | fn test_unknown_claims_serde() {
function test_audience (line 524) | fn test_audience() {
type TestClaims (line 607) | struct TestClaims {
function test_additional_claims (line 613) | fn test_additional_claims() {
type AllOtherClaims (line 651) | struct AllOtherClaims(HashMap<String, serde_json::Value>);
function test_catch_all_additional_claims (line 655) | fn test_catch_all_additional_claims() {
function test_audiences_claim (line 674) | fn test_audiences_claim() {
function test_issuer_claim (line 697) | fn test_issuer_claim() {
FILE: src/jwt/mod.rs
type InvalidJsonWebTokenTypeError (line 27) | pub struct InvalidJsonWebTokenTypeError {
type NormalizedJsonWebTokenType (line 68) | pub struct NormalizedJsonWebTokenType(String);
type Target (line 71) | type Target = String;
method deref (line 72) | fn deref(&self) -> &String {
type Error (line 89) | type Error = InvalidJsonWebTokenTypeError;
method try_from (line 95) | fn try_from(t: &JsonWebTokenType) -> Result<NormalizedJsonWebTokenType...
method from (line 77) | fn from(t: NormalizedJsonWebTokenType) -> String {
method from (line 83) | fn from(t: NormalizedJsonWebTokenType) -> JsonWebTokenType {
type JsonWebTokenAlgorithm (line 128) | pub enum JsonWebTokenAlgorithm<JE, JS>
function deserialize (line 151) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
method serialize (line 181) | fn serialize<SE>(&self, serializer: SE) -> Result<SE::Ok, SE::Error>
type JsonWebTokenHeader (line 195) | pub struct JsonWebTokenHeader<JE, JS>
type JsonWebTokenPayloadSerde (line 217) | pub trait JsonWebTokenPayloadSerde<P>: Debug
method deserialize (line 221) | fn deserialize<DE: serde::de::Error>(payload: &[u8]) -> Result<P, DE>;
method serialize (line 222) | fn serialize(payload: &P) -> Result<String, serde_json::Error>;
type JsonWebTokenJsonPayloadSerde (line 226) | pub struct JsonWebTokenJsonPayloadSerde;
method deserialize (line 231) | fn deserialize<DE: serde::de::Error>(payload: &[u8]) -> Result<P, DE> {
method serialize (line 236) | fn serialize(payload: &P) -> Result<String, serde_json::Error> {
type JsonWebTokenAccess (line 243) | pub trait JsonWebTokenAccess<JE, JS, P>
method unverified_header (line 251) | fn unverified_header(&self) -> &JsonWebTokenHeader<JE, JS>;
method unverified_payload (line 252) | fn unverified_payload(self) -> Self::ReturnType;
method unverified_payload_ref (line 253) | fn unverified_payload_ref(&self) -> &P;
method payload (line 255) | fn payload<K>(
method signing_alg (line 263) | fn signing_alg(&self) -> Result<&JS, SignatureVerificationError> {
type JsonWebTokenError (line 292) | pub enum JsonWebTokenError {
type JsonWebToken (line 302) | pub struct JsonWebToken<JE, JS, P, S>
function new (line 322) | pub fn new<SK>(payload: P, signing_key: &SK, alg: &JS) -> Result<Self, J...
type ReturnType (line 366) | type ReturnType = P;
function unverified_header (line 367) | fn unverified_header(&self) -> &JsonWebTokenHeader<JE, JS> {
function unverified_payload (line 370) | fn unverified_payload(self) -> Self::ReturnType {
function unverified_payload_ref (line 373) | fn unverified_payload_ref(&self) -> &P {
function payload (line 376) | fn payload<K>(
type ReturnType (line 400) | type ReturnType = &'a P;
function unverified_header (line 401) | fn unverified_header(&self) -> &JsonWebTokenHeader<JE, JS> {
function unverified_payload (line 404) | fn unverified_payload(self) -> Self::ReturnType {
function unverified_payload_ref (line 407) | fn unverified_payload_ref(&self) -> &P {
function payload (line 410) | fn payload<K>(
function deserialize (line 433) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
method serialize (line 522) | fn serialize<SE>(&self, serializer: SE) -> Result<SE::Ok, SE::Error>
FILE: src/jwt/tests.rs
type CoreAlgorithm (line 15) | type CoreAlgorithm =
constant TEST_JWT (line 18) | pub const TEST_JWT: &str =
constant TEST_JWT_PAYLOAD (line 28) | const TEST_JWT_PAYLOAD: &str = "It\u{2019}s a dangerous business, Frodo,...
constant TEST_RSA_PUB_KEY (line 33) | pub const TEST_RSA_PUB_KEY: &str = "{
constant TEST_ED_PUB_KEY_ED25519 (line 47) | pub const TEST_ED_PUB_KEY_ED25519: &str = r#"{
constant TEST_EC_PUB_KEY_P256 (line 55) | pub const TEST_EC_PUB_KEY_P256: &str = r#"{
constant TEST_EC_PUB_KEY_P384 (line 63) | pub const TEST_EC_PUB_KEY_P384: &str = r#"{
constant TEST_RSA_PRIV_KEY (line 74) | pub const TEST_RSA_PRIV_KEY: &str = "-----BEGIN RSA PRIVATE KEY-----\n\
function test_jwt_algorithm_deserialization (line 103) | fn test_jwt_algorithm_deserialization() {
function test_jwt_algorithm_serialization (line 130) | fn test_jwt_algorithm_serialization() {
type JsonWebTokenStringPayloadSerde (line 167) | pub struct JsonWebTokenStringPayloadSerde;
method deserialize (line 169) | fn deserialize<DE: serde::de::Error>(payload: &[u8]) -> Result<String,...
method serialize (line 172) | fn serialize(payload: &String) -> Result<String, serde_json::Error> {
function test_jwt_basic (line 178) | fn test_jwt_basic() {
function test_new_jwt (line 234) | fn test_new_jwt() {
function test_invalid_signature (line 260) | fn test_invalid_signature() {
function test_invalid_deserialization (line 288) | fn test_invalid_deserialization() {
function test_json_web_token_type_normalization (line 369) | fn test_json_web_token_type_normalization() {
FILE: src/logout.rs
type LogoutProviderMetadata (line 22) | pub struct LogoutProviderMetadata<A>
type ProviderMetadataWithLogout (line 40) | pub type ProviderMetadataWithLogout = ProviderMetadata<
type LogoutRequest (line 56) | pub struct LogoutRequest {
method from (line 72) | fn from(value: EndSessionUrl) -> Self {
method set_id_token_hint (line 83) | pub fn set_id_token_hint<AC, GC, JE, JS>(
method set_logout_hint (line 100) | pub fn set_logout_hint(mut self, logout_hint: LogoutHint) -> Self {
method set_client_id (line 109) | pub fn set_client_id(mut self, client_id: ClientId) -> Self {
method set_post_logout_redirect_uri (line 116) | pub fn set_post_logout_redirect_uri(mut self, redirect_uri: PostLogout...
method set_state (line 123) | pub fn set_state(mut self, state: CsrfToken) -> Self {
method add_ui_locale (line 132) | pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self {
method http_get_url (line 139) | pub fn http_get_url(self) -> Url {
type LogoutRequestParameters (line 62) | struct LogoutRequestParameters {
function test_end_session_endpoint_deserialization (line 188) | fn test_end_session_endpoint_deserialization() {
function test_logout_request_with_no_parameters (line 245) | fn test_logout_request_with_no_parameters() {
function test_logout_request_with_all_parameters (line 262) | fn test_logout_request_with_all_parameters() {
FILE: src/registration/mod.rs
type AdditionalClientMetadata (line 36) | pub trait AdditionalClientMetadata: Debug + DeserializeOwned + Serialize {}
type EmptyAdditionalClientMetadata (line 42) | pub struct EmptyAdditionalClientMetadata {}
type ClientMetadata (line 47) | pub struct ClientMetadata<A, AT, CA, G, JE, JK, K, RT, S>
function new (line 84) | pub fn new(redirect_uris: Vec<RedirectUrl>, additional_metadata: A) -> S...
function additional_metadata (line 157) | pub fn additional_metadata(&self) -> &A {
function additional_metadata_mut (line 161) | pub fn additional_metadata_mut(&mut self) -> &mut A {
type StandardClientMetadata (line 167) | struct StandardClientMetadata<AT, CA, G, JE, JK, K, RT, S>
function deserialize (line 227) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
method serialize (line 447) | fn serialize<SE>(&self, serializer: SE) -> Result<SE::Ok, SE::Error>
type ClientRegistrationRequest (line 490) | pub struct ClientRegistrationRequest<AC, AR, AT, CA, ET, G, JE, JK, K, R...
function new (line 528) | pub fn new(redirect_uris: Vec<RedirectUrl>, additional_metadata: AC) -> ...
function register (line 538) | pub fn register<C>(
function register_async (line 560) | pub fn register_async<'c, C>(
function prepare_registration (line 584) | fn prepare_registration<RE>(
function register_response (line 611) | fn register_response<RE>(
function client_metadata (line 666) | pub fn client_metadata(&self) -> &ClientMetadata<AC, AT, CA, G, JE, JK, ...
function initial_access_token (line 671) | pub fn initial_access_token(&self) -> Option<&AccessToken> {
function set_initial_access_token (line 675) | pub fn set_initial_access_token(mut self, access_token: Option<AccessTok...
function additional_metadata (line 716) | pub fn additional_metadata(&self) -> &AC {
function additional_metadata_mut (line 720) | pub fn additional_metadata_mut(&mut self) -> &mut AC {
type AdditionalClientRegistrationResponse (line 726) | pub trait AdditionalClientRegistrationResponse: Debug + DeserializeOwned...
type EmptyAdditionalClientRegistrationResponse (line 732) | pub struct EmptyAdditionalClientRegistrationResponse {}
type ClientRegistrationResponse (line 739) | pub struct ClientRegistrationResponse<AC, AR, AT, CA, G, JE, JK, K, RT, S>
function new (line 785) | pub fn new(
function from_client_metadata (line 804) | pub fn from_client_metadata(
function additional_metadata (line 869) | pub fn additional_metadata(&self) -> &AC {
function additional_metadata_mut (line 873) | pub fn additional_metadata_mut(&mut self) -> &mut AC {
function additional_response (line 878) | pub fn additional_response(&self) -> &AR {
function additional_response_mut (line 882) | pub fn additional_response_mut(&mut self) -> &mut AR {
type RegisterErrorResponseType (line 890) | pub trait RegisterErrorResponseType: ErrorResponseType + 'static {}
type ClientRegistrationError (line 895) | pub enum ClientRegistrationError<T, RE>
FILE: src/registration/tests.rs
function test_metadata_serialization (line 20) | fn test_metadata_serialization() {
function test_metadata_serialization_minimal (line 267) | fn test_metadata_serialization_minimal() {
function test_response_serialization (line 315) | fn test_response_serialization() {
FILE: src/token.rs
type TokenResponse (line 8) | pub trait TokenResponse<AC, GC, JE, JS>: OAuth2TokenResponse
method id_token (line 19) | fn id_token(&self) -> Option<&IdToken<AC, GC, JE, JS>>;
function id_token (line 32) | fn id_token(&self) -> Option<&IdToken<AC, GC, JE, JS>> {
FILE: src/types/jwk.rs
type JsonWebKey (line 16) | pub trait JsonWebKey: Clone + Debug + DeserializeOwned + Serialize + 'st...
method key_id (line 24) | fn key_id(&self) -> Option<&JsonWebKeyId>;
method key_type (line 27) | fn key_type(&self) -> &<Self::SigningAlgorithm as JwsSigningAlgorithm>...
method key_use (line 31) | fn key_use(&self) -> Option<&Self::KeyUse>;
method signing_alg (line 37) | fn signing_alg(&self) -> JsonWebKeyAlgorithm<&Self::SigningAlgorithm>;
method new_symmetric (line 40) | fn new_symmetric(key: Vec<u8>) -> Self;
method verify_signature (line 46) | fn verify_signature(
method hash_bytes (line 64) | fn hash_bytes(&self, bytes: &[u8], alg: &Self::SigningAlgorithm) -> Re...
type JsonWebKeyAlgorithm (line 69) | pub enum JsonWebKeyAlgorithm<A: Debug> {
type PrivateSigningKey (line 79) | pub trait PrivateSigningKey {
method sign (line 84) | fn sign(
method as_verification_key (line 91) | fn as_verification_key(&self) -> Self::VerificationKey;
type JsonWebKeyType (line 95) | pub trait JsonWebKeyType:
type JsonWebKeyUse (line 101) | pub trait JsonWebKeyUse: Debug + DeserializeOwned + Serialize + 'static {
method allows_signature (line 103) | fn allows_signature(&self) -> bool;
method allows_encryption (line 106) | fn allows_encryption(&self) -> bool;
type JweContentEncryptionAlgorithm (line 110) | pub trait JweContentEncryptionAlgorithm:
method key_type (line 117) | fn key_type(&self) -> Result<Self::KeyType, String>;
type JweKeyManagementAlgorithm (line 121) | pub trait JweKeyManagementAlgorithm: Debug + DeserializeOwned + Serializ...
type JwsSigningAlgorithm (line 126) | pub trait JwsSigningAlgorithm:
method key_type (line 134) | fn key_type(&self) -> Option<Self::KeyType>;
method uses_shared_secret (line 137) | fn uses_shared_secret(&self) -> bool;
method rsa_sha_256 (line 143) | fn rsa_sha_256() -> Self;
FILE: src/types/jwks.rs
type JsonWebKeySet (line 22) | pub struct JsonWebKeySet<K>
function check_key_compatibility (line 36) | pub(crate) fn check_key_compatibility<K>(
function new (line 69) | pub fn new(keys: Vec<K>) -> Self {
function filter_keys (line 74) | pub(crate) fn filter_keys(
function fetch (line 96) | pub fn fetch<C>(
function fetch_async (line 113) | pub fn fetch_async<'c, C>(
function fetch_request (line 132) | fn fetch_request(url: &JsonWebKeySetUrl) -> Result<HttpRequest, http::Er...
function fetch_response (line 140) | fn fetch_response<RE>(http_response: HttpResponse) -> Result<Self, Disco...
function keys (line 171) | pub fn keys(&self) -> &Vec<K> {
method clone (line 179) | fn clone(&self) -> Self {
method default (line 187) | fn default() -> Self {
FILE: src/types/localized.rs
method as_ref (line 13) | fn as_ref(&self) -> &str {
method fmt (line 18) | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
function split_language_tag_key (line 23) | pub(crate) fn split_language_tag_key(key: &str) -> (&str, Option<Languag...
function join_language_tag_key (line 37) | pub(crate) fn join_language_tag_key<'a>(
type LocalizedClaim (line 54) | pub struct LocalizedClaim<T>(HashMap<LanguageTag, T>, Option<T>);
function new (line 57) | pub fn new() -> Self {
function contains_key (line 62) | pub fn contains_key(&self, locale: Option<&LanguageTag>) -> bool {
function get (line 71) | pub fn get(&self, locale: Option<&LanguageTag>) -> Option<&T> {
function iter (line 80) | pub fn iter(&self) -> impl Iterator<Item = (Option<&LanguageTag>, &T)> {
function insert (line 91) | pub fn insert(&mut self, locale: Option<LanguageTag>, value: T) -> Optio...
function remove (line 103) | pub fn remove(&mut self, locale: Option<&LanguageTag>) -> Option<T> {
function flatten_or_none (line 112) | pub(crate) fn flatten_or_none(self) -> Option<LocalizedClaim<T>> {
method default (line 129) | fn default() -> Self {
function from (line 134) | fn from(default: T) -> Self {
function from_iter (line 139) | fn from_iter<I: IntoIterator<Item = (Option<LanguageTag>, T)>>(iter: I) ...
type Item (line 154) | type Item = <LocalizedClaimIterator<T> as Iterator>::Item;
type IntoIter (line 155) | type IntoIter = LocalizedClaimIterator<T>;
method into_iter (line 157) | fn into_iter(self) -> Self::IntoIter {
type LocalizedClaimIterator (line 171) | pub struct LocalizedClaimIterator<T> {
type Item (line 175) | type Item = (Option<LanguageTag>, T);
method next (line 176) | fn next(&mut self) -> Option<Self::Item> {
FILE: src/types/mod.rs
type ApplicationType (line 25) | pub trait ApplicationType: Debug + DeserializeOwned + Serialize + 'stati...
type AuthDisplay (line 29) | pub trait AuthDisplay: AsRef<str> + Debug + DeserializeOwned + Serialize...
type AuthPrompt (line 32) | pub trait AuthPrompt: AsRef<str> + 'static {}
type ClaimName (line 35) | pub trait ClaimName: Debug + DeserializeOwned + Serialize + 'static {}
type ClaimType (line 38) | pub trait ClaimType: Debug + DeserializeOwned + Serialize + 'static {}
type ClientAuthMethod (line 41) | pub trait ClientAuthMethod: Debug + DeserializeOwned + Serialize + 'stat...
type GrantType (line 44) | pub trait GrantType: Debug + DeserializeOwned + Serialize + 'static {}
type SigningError (line 49) | pub enum SigningError {
type ResponseMode (line 63) | pub trait ResponseMode: Debug + DeserializeOwned + Serialize + 'static {}
type ResponseType (line 67) | pub trait ResponseType: AsRef<str> + Debug + DeserializeOwned + Serializ...
method to_oauth2 (line 70) | fn to_oauth2(&self) -> oauth2::ResponseType;
type SubjectIdentifierType (line 74) | pub trait SubjectIdentifierType: Debug + DeserializeOwned + Serialize + ...
method as_ref (line 83) | fn as_ref(&self) -> &str {
type ResponseTypes (line 404) | pub struct ResponseTypes<RT: ResponseType>(
function new (line 413) | pub fn new(s: Vec<RT>) -> Self {
type Target (line 418) | type Target = Vec<RT>;
method deref (line 419) | fn deref(&self) -> &Vec<RT> {
FILE: src/types/tests.rs
function test_issuer_url_append (line 4) | fn test_issuer_url_append() {
function test_url_serialize (line 40) | fn test_url_serialize() {
function test_string_bool_parse (line 61) | fn test_string_bool_parse() {
FILE: src/user_info.rs
function user_info_impl (line 87) | pub(crate) fn user_info_impl<'a>(
type UserInfoRequest (line 109) | pub struct UserInfoRequest<'a, JE, K>
function request (line 131) | pub fn request<AC, GC, C>(
function request_async (line 152) | pub fn request_async<'c, AC, C, GC>(
function prepare_request (line 176) | fn prepare_request(&self) -> Result<HttpRequest, http::Error> {
function user_info_response (line 191) | fn user_info_response<AC, GC, RE>(
function require_signed_response (line 248) | pub fn require_signed_response(mut self, require_signed_response: bool) ...
function require_issuer_match (line 257) | pub fn require_issuer_match(mut self, iss_required: bool) -> Self {
function require_audience_match (line 268) | pub fn require_audience_match(mut self, aud_required: bool) -> Self {
function set_response_type (line 276) | pub fn set_response_type(mut self, response_type: UserInfoResponseType) ...
type UserInfoClaims (line 284) | pub struct UserInfoClaims<AC: AdditionalClaims, GC: GenderClaim>(UserInf...
function new (line 291) | pub fn new(standard_claims: StandardClaims<GC>, additional_claims: AC) -...
function from_json (line 304) | pub fn from_json<RE>(
function subject (line 343) | pub fn subject(&self) -> &SubjectIdentifier {
function set_subject (line 347) | pub fn set_subject(&mut self, subject: SubjectIdentifier) {
function standard_claims (line 379) | pub fn standard_claims(&self) -> &StandardClaims<GC> {
function additional_claims (line 384) | pub fn additional_claims(&self) -> &AC {
function additional_claims_mut (line 388) | pub fn additional_claims_mut(&mut self) -> &mut AC {
type UserInfoClaimsImpl (line 395) | pub(crate) struct UserInfoClaimsImpl<AC, GC>
method audiences (line 421) | fn audiences(&self) -> Option<&Vec<Audience>> {
method audiences (line 430) | fn audiences(&self) -> Option<&Vec<Audience>> {
method issuer (line 440) | fn issuer(&self) -> Option<&IssuerUrl> {
method issuer (line 449) | fn issuer(&self) -> Option<&IssuerUrl> {
type UserInfoJsonWebToken (line 456) | pub struct UserInfoJsonWebToken<
function new (line 474) | pub fn new<S>(
function claims (line 487) | pub fn claims<K>(
type UserInfoResponseType (line 508) | pub enum UserInfoResponseType {
type UserInfoError (line 518) | pub enum UserInfoError<RE>
type TestClaims (line 550) | struct TestClaims {
function test_additional_claims (line 556) | fn test_additional_claims() {
type AllOtherClaims (line 593) | struct AllOtherClaims(HashMap<String, serde_json::Value>);
function test_catch_all_additional_claims (line 597) | fn test_catch_all_additional_claims() {
FILE: src/verification/mod.rs
type AudiencesClaim (line 25) | pub(crate) trait AudiencesClaim {
method audiences (line 26) | fn audiences(&self) -> Option<&Vec<Audience>>;
type IssuerClaim (line 29) | pub(crate) trait IssuerClaim {
method issuer (line 30) | fn issuer(&self) -> Option<&IssuerUrl>;
type ClaimsVerificationError (line 36) | pub enum ClaimsVerificationError {
type SignatureVerificationError (line 72) | pub enum SignatureVerificationError {
type JwtClaimsVerifier (line 117) | pub(crate) struct JwtClaimsVerifier<'a, K>
function new (line 136) | pub fn new(client_id: ClientId, issuer: IssuerUrl, signature_keys: JsonW...
function require_audience_match (line 167) | pub fn require_audience_match(mut self, aud_required: bool) -> Self {
function require_issuer_match (line 172) | pub fn require_issuer_match(mut self, iss_required: bool) -> Self {
function require_signature_check (line 177) | pub fn require_signature_check(mut self, sig_required: bool) -> Self {
function set_allowed_algs (line 182) | pub fn set_allowed_algs<I>(mut self, algs: I) -> Self
function allow_any_alg (line 189) | pub fn allow_any_alg(mut self) -> Self {
function set_allowed_jose_types (line 197) | pub fn set_allowed_jose_types<I>(mut self, types: I) -> Self
function allow_all_jose_types (line 204) | pub fn allow_all_jose_types(mut self) -> Self {
function set_client_secret (line 209) | pub fn set_client_secret(mut self, client_secret: ClientSecret) -> Self {
function set_other_audience_verifier_fn (line 214) | pub fn set_other_audience_verifier_fn<T>(mut self, other_aud_verifier_fn...
function validate_jose_header (line 222) | fn validate_jose_header<JE>(
function verified_claims (line 283) | pub fn verified_claims<A, C, JE, T>(&self, jwt: A) -> Result<T, ClaimsVe...
function signing_key (line 459) | pub(crate) fn signing_key<'b>(
type NonceVerifier (line 495) | pub trait NonceVerifier {
method verify (line 499) | fn verify(self, nonce: Option<&Nonce>) -> Result<(), String>;
method verify (line 503) | fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> {
method verify (line 520) | fn verify(self, nonce: Option<&Nonce>) -> Result<(), String> {
type IdTokenVerifier (line 527) | pub struct IdTokenVerifier<'a, K>
function new (line 544) | fn new(jwt_verifier: JwtClaimsVerifier<'a, K>) -> Self {
function new_public_client (line 558) | pub fn new_public_client(
function new_insecure_without_verification (line 568) | pub fn new_insecure_without_verification() -> Self {
function new_confidential_client (line 586) | pub fn new_confidential_client(
function set_allowed_algs (line 599) | pub fn set_allowed_algs<I>(mut self, algs: I) -> Self
function allow_any_alg (line 608) | pub fn allow_any_alg(mut self) -> Self {
function set_allowed_jose_types (line 616) | pub fn set_allowed_jose_types<I>(mut self, types: I) -> Self
function allow_all_jose_types (line 625) | pub fn allow_all_jose_types(mut self) -> Self {
function set_auth_context_verifier_fn (line 634) | pub fn set_auth_context_verifier_fn<T>(mut self, acr_verifier_fn: T) -> ...
function set_auth_time_verifier_fn (line 646) | pub fn set_auth_time_verifier_fn<T>(mut self, auth_time_verifier_fn: T) ...
function enable_signature_check (line 658) | pub fn enable_signature_check(mut self) -> Self {
function insecure_disable_signature_check (line 670) | pub fn insecure_disable_signature_check(mut self) -> Self {
function require_issuer_match (line 676) | pub fn require_issuer_match(mut self, iss_required: bool) -> Self {
function require_audience_match (line 682) | pub fn require_audience_match(mut self, aud_required: bool) -> Self {
function set_time_fn (line 690) | pub fn set_time_fn<T>(mut self, time_fn: T) -> Self
function set_issue_time_verifier_fn (line 702) | pub fn set_issue_time_verifier_fn<T>(mut self, iat_verifier_fn: T) -> Self
function set_other_audience_verifier_fn (line 718) | pub fn set_other_audience_verifier_fn<T>(mut self, other_aud_verifier_fn...
function verified_claims (line 728) | pub(crate) fn verified_claims<'b, AC, GC, JE, N>(
function verified_claims_owned (line 756) | pub(crate) fn verified_claims_owned<AC, GC, JE, N>(
function verify_claims (line 784) | fn verify_claims<AC, GC, N>(
type UserInfoVerifier (line 869) | pub struct UserInfoVerifier<'a, JE, K>
function new (line 888) | pub fn new(
function expected_subject (line 901) | pub(crate) fn expected_subject(&self) -> Option<&SubjectIdentifier> {
function require_issuer_match (line 906) | pub fn require_issuer_match(mut self, iss_required: bool) -> Self {
function require_audience_match (line 912) | pub fn require_audience_match(mut self, aud_required: bool) -> Self {
function verified_claims (line 917) | pub(crate) fn verified_claims<AC, GC>(
FILE: src/verification/tests.rs
type CoreJsonWebTokenHeader (line 24) | type CoreJsonWebTokenHeader =
type CoreJwtClaimsVerifier (line 27) | type CoreJwtClaimsVerifier<'a> = JwtClaimsVerifier<'a, CoreJsonWebKey>;
function assert_unsupported (line 29) | fn assert_unsupported<T>(result: Result<T, ClaimsVerificationError>, exp...
function test_jose_header (line 40) | fn test_jose_header() {
type TestClaims (line 290) | struct TestClaims {
method audiences (line 296) | fn audiences(&self) -> Option<&Vec<Audience>> {
method issuer (line 301) | fn issuer(&self) -> Option<&IssuerUrl> {
type TestClaimsJsonWebToken (line 305) | type TestClaimsJsonWebToken = JsonWebToken<
function test_jwt_verified_claims (line 313) | fn test_jwt_verified_claims() {
type CoreIdTokenJwt (line 793) | type CoreIdTokenJwt = JsonWebToken<
function test_id_token_verified_claims (line 801) | fn test_id_token_verified_claims() {
function test_new_id_token (line 1113) | fn test_new_id_token() {
function test_user_info_verified_claims (line 1186) | fn test_user_info_verified_claims() {
function test_new_user_info_claims (line 1315) | fn test_new_user_info_claims() {
FILE: tests/rp_certification_code.rs
type TestState (line 29) | struct TestState {
method init (line 46) | pub fn init<F>(test_id: &'static str, reg_request_fn: F) -> Self
method access_token (line 75) | pub fn access_token(&self) -> &AccessToken {
method authorize (line 79) | pub fn authorize(mut self, scopes: &[Scope]) -> Self {
method exchange_code (line 151) | pub fn exchange_code(mut self) -> Self {
method id_token (line 179) | pub fn id_token(&self) -> &CoreIdToken {
method id_token_verifier (line 183) | pub fn id_token_verifier(&self, jwks: CoreJsonWebKeySet) -> CoreIdToke...
method id_token_claims (line 195) | pub fn id_token_claims(&self) -> &CoreIdTokenClaims {
method id_token_claims_failure (line 202) | pub fn id_token_claims_failure(&self) -> ClaimsVerificationError {
method jwks (line 209) | pub fn jwks(&self) -> CoreJsonWebKeySet {
method set_auth_type (line 214) | pub fn set_auth_type(mut self, auth_type: AuthType) -> Self {
method user_info_claims (line 219) | pub fn user_info_claims(&self) -> CoreUserInfoClaims {
method user_info_claims_failure (line 231) | pub fn user_info_claims_failure(&self) -> UserInfoError<HttpClientErro...
function rp_response_type_code (line 250) | fn rp_response_type_code() {
function rp_scope_userinfo_claims (line 264) | fn rp_scope_userinfo_claims() {
function rp_nonce_invalid (line 300) | fn rp_nonce_invalid() {
function rp_token_endpoint_client_secret_basic (line 317) | fn rp_token_endpoint_client_secret_basic() {
function rp_token_endpoint_client_secret_post (line 333) | fn rp_token_endpoint_client_secret_post() {
function rp_id_token_kid_absent_single_jwks (line 349) | fn rp_id_token_kid_absent_single_jwks() {
function rp_id_token_iat (line 362) | fn rp_id_token_iat() {
function rp_id_token_aud (line 387) | fn rp_id_token_aud() {
function rp_id_token_kid_absent_multiple_jwks (line 404) | fn rp_id_token_kid_absent_multiple_jwks() {
function rp_id_token_sig_none (line 421) | fn rp_id_token_sig_none() {
function rp_id_token_sig_rs256 (line 441) | fn rp_id_token_sig_rs256() {
function rp_id_token_sig_hs256 (line 454) | fn rp_id_token_sig_hs256() {
function rp_id_token_sub (line 473) | fn rp_id_token_sub() {
function rp_id_token_bad_sig_rs256 (line 498) | fn rp_id_token_bad_sig_rs256() {
function rp_id_token_bad_sig_hs256 (line 515) | fn rp_id_token_bad_sig_hs256() {
function rp_id_token_issuer_mismatch (line 539) | fn rp_id_token_issuer_mismatch() {
function rp_userinfo_bad_sub_claim (line 556) | fn rp_userinfo_bad_sub_claim() {
function rp_userinfo_bearer_header (line 574) | fn rp_userinfo_bearer_header() {
function rp_userinfo_sig (line 588) | fn rp_userinfo_sig() {
FILE: tests/rp_certification_dynamic.rs
function rp_discovery_openid_configuration (line 13) | fn rp_discovery_openid_configuration() {
function rp_registration_dynamic (line 73) | fn rp_registration_dynamic() {
FILE: tests/rp_common.rs
constant CERTIFICATION_BASE_URL (line 17) | pub const CERTIFICATION_BASE_URL: &str = "https://rp.certification.openi...
constant RP_CONTACT_EMAIL (line 18) | pub const RP_CONTACT_EMAIL: &str = "ramos@cs.stanford.edu";
constant RP_NAME (line 19) | pub const RP_NAME: &str = "openidconnect-rs";
constant RP_REDIRECT_URI (line 20) | pub const RP_REDIRECT_URI: &str = "http://localhost:8080";
function get_test_id (line 28) | pub fn get_test_id() -> &'static str {
function set_test_id (line 32) | pub fn set_test_id(test_id: &'static str) {
function _init_log (line 65) | fn _init_log() {
function init_log (line 70) | pub fn init_log(test_id: &'static str) {
function clone_request (line 77) | pub(crate) fn clone_request(request: &HttpRequest) -> HttpRequest {
function http_client (line 89) | pub fn http_client(request: HttpRequest) -> Result<HttpResponse, HttpCli...
type PanicIfFail (line 116) | pub trait PanicIfFail<T, F>
method panic_if_fail (line 120) | fn panic_if_fail(self, msg: &'static str) -> T;
function panic_if_fail (line 126) | fn panic_if_fail(self, msg: &'static str) -> T {
function issuer_url (line 144) | pub fn issuer_url(test_id: &str) -> IssuerUrl {
function get_provider_metadata (line 152) | pub fn get_provider_metadata(test_id: &str) -> CoreProviderMetadata {
function register_client (line 160) | pub fn register_client<F>(
Condensed preview — 44 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (760K chars).
[
{
"path": ".codecov.yml",
"chars": 22,
"preview": "ignore:\n - \"tests/**\"\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 20,
"preview": "github: [ramosbugs]\n"
},
{
"path": ".github/workflows/main.yml",
"chars": 3734,
"preview": "name: CI\n\n# Controls when the workflow will run\non:\n # Triggers the workflow on push or pull request events but only fo"
},
{
"path": ".gitignore",
"chars": 59,
"preview": "/target/\n**/*.rs.bk\nCargo.lock\n*~\n.DS_Store\n.idea/**\n*.iml\n"
},
{
"path": "Cargo.toml",
"chars": 2049,
"preview": "[package]\nname = \"openidconnect\"\nversion = \"4.0.1\"\nauthors = [\"David A. Ramos <ramos@cs.stanford.edu>\"]\ndescription = \"O"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2018 David Ramos\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 3158,
"preview": "# [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) Library for Rust\n\n[ {\n let serialized_"
},
{
"path": "src/discovery/mod.rs",
"chars": 18196,
"preview": "use crate::http_utils::{check_content_type, MIME_TYPE_JSON};\nuse crate::{\n AsyncHttpClient, AuthDisplay, AuthUrl, Aut"
},
{
"path": "src/discovery/tests.rs",
"chars": 43337,
"preview": "use crate::core::{\n CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType,\n CoreJweCo"
},
{
"path": "src/helpers.rs",
"chars": 15590,
"preview": "use crate::types::localized::join_language_tag_key;\nuse crate::{LanguageTag, LocalizedClaim};\n\nuse chrono::{DateTime, Ti"
},
{
"path": "src/http_utils.rs",
"chars": 1843,
"preview": "use crate::AccessToken;\n\nuse http::header::{HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_TYPE};\n\npub const"
},
{
"path": "src/id_token/mod.rs",
"chars": 15120,
"preview": "use crate::helpers::{deserialize_string_or_vec, FilteredFlatten, Timestamp};\nuse crate::jwt::JsonWebTokenAccess;\nuse cra"
},
{
"path": "src/id_token/tests.rs",
"chars": 27835,
"preview": "use crate::claims::{AdditionalClaims, EmptyAdditionalClaims, StandardClaims};\nuse crate::core::{\n CoreGenderClaim, Co"
},
{
"path": "src/jwt/mod.rs",
"chars": 19730,
"preview": "use crate::{\n JsonWebKey, JsonWebKeyId, JweContentEncryptionAlgorithm, JwsSigningAlgorithm,\n PrivateSigningKey, Si"
},
{
"path": "src/jwt/tests.rs",
"chars": 15302,
"preview": "use crate::core::{\n CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm,\n CoreRsaPrivateSig"
},
{
"path": "src/lib.rs",
"chars": 31525,
"preview": "#![warn(missing_docs)]\n#![allow(clippy::unreadable_literal, clippy::type_complexity)]\n#![cfg_attr(test, allow(clippy::co"
},
{
"path": "src/logout.rs",
"chars": 11959,
"preview": "use crate::core::{\n CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType,\n CoreJsonW"
},
{
"path": "src/macros.rs",
"chars": 23125,
"preview": "/// Copied from oauth2-rs crate (not part of that crate's stable public interface).\nmacro_rules! new_type {\n // Conve"
},
{
"path": "src/registration/mod.rs",
"chars": 41901,
"preview": "use crate::helpers::{DeserializeMapField, Timestamp};\nuse crate::http_utils::{auth_bearer, check_content_type, MIME_TYPE"
},
{
"path": "src/registration/tests.rs",
"chars": 24703,
"preview": "use crate::core::{\n CoreApplicationType, CoreClientAuthMethod, CoreClientMetadata, CoreClientRegistrationResponse,\n "
},
{
"path": "src/token.rs",
"chars": 1224,
"preview": "use crate::{\n AdditionalClaims, ExtraTokenFields, GenderClaim, IdToken, IdTokenFields,\n JweContentEncryptionAlgori"
},
{
"path": "src/types/jwk.rs",
"chars": 5602,
"preview": "use crate::{SignatureVerificationError, SigningError};\n\nuse serde::de::DeserializeOwned;\nuse serde::{Deserialize, Serial"
},
{
"path": "src/types/jwks.rs",
"chars": 6204,
"preview": "use crate::http_utils::{check_content_type, MIME_TYPE_JSON, MIME_TYPE_JWKS};\nuse crate::types::jwk::{JsonWebKey, JsonWeb"
},
{
"path": "src/types/localized.rs",
"chars": 5451,
"preview": "use serde::{Deserialize, Serialize};\n\nuse std::borrow::Cow;\nuse std::collections::HashMap;\nuse std::fmt::Display;\n\nnew_t"
},
{
"path": "src/types/mod.rs",
"chars": 13953,
"preview": "use crate::types::jwk::JsonWebKey;\nuse crate::{AccessToken, AuthorizationCode};\n\nuse base64::prelude::BASE64_URL_SAFE_NO"
},
{
"path": "src/types/tests.rs",
"chars": 2229,
"preview": "use crate::IssuerUrl;\n\n#[test]\nfn test_issuer_url_append() {\n assert_eq!(\n \"http://example.com/.well-known/ope"
},
{
"path": "src/user_info.rs",
"chars": 20848,
"preview": "use crate::helpers::{deserialize_string_or_vec_opt, FilteredFlatten};\nuse crate::http_utils::{auth_bearer, content_type_"
},
{
"path": "src/verification/mod.rs",
"chars": 39418,
"preview": "use crate::jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde, NormalizedJsonWebTokenType};\nuse crate::user_info::UserInfo"
},
{
"path": "src/verification/tests.rs",
"chars": 56062,
"preview": "use crate::core::{\n CoreIdToken, CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKey, CoreJsonWebKeySet,\n CoreJ"
},
{
"path": "tests/rp_certification_code.rs",
"chars": 19793,
"preview": "#![allow(clippy::expect_fun_call)]\n\nuse crate::rp_common::{\n get_provider_metadata, http_client, init_log, issuer_url"
},
{
"path": "tests/rp_certification_dynamic.rs",
"chars": 4540,
"preview": "#![allow(clippy::cognitive_complexity)]\n\nuse crate::rp_common::{\n get_provider_metadata, init_log, issuer_url, regist"
},
{
"path": "tests/rp_common.rs",
"chars": 5723,
"preview": "#![allow(clippy::cognitive_complexity, clippy::expect_fun_call)]\n\nuse log::{error, warn};\nuse openidconnect::core::{\n "
}
]
About this extraction
This page contains the full source code of the ramosbugs/openidconnect-rs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 44 files (712.2 KB), approximately 181.7k tokens, and a symbol index with 662 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.