├── .codecov.yml ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── src ├── core │ ├── tests.rs │ └── crypto.rs ├── token.rs ├── http_utils.rs ├── types │ ├── tests.rs │ ├── jwk.rs │ ├── localized.rs │ ├── jwks.rs │ └── mod.rs ├── logout.rs ├── jwt │ └── tests.rs ├── id_token │ └── mod.rs ├── helpers.rs ├── discovery │ └── mod.rs └── claims.rs ├── LICENSE ├── Cargo.toml ├── README.md ├── tests ├── rp_certification_dynamic.rs └── rp_common.rs ├── examples ├── okta_device_grant.rs ├── gitlab.rs └── google.rs └── UPGRADE.md /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "tests/**" 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ramosbugs] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | *~ 5 | .DS_Store 6 | .idea/** 7 | *.iml 8 | -------------------------------------------------------------------------------- /src/core/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{CoreGrantType, CoreJwsSigningAlgorithm}; 2 | 3 | #[test] 4 | fn test_grant_type_serialize() { 5 | let serialized_implicit = serde_json::to_string(&CoreGrantType::Implicit).unwrap(); 6 | assert_eq!("\"implicit\"", serialized_implicit); 7 | assert_eq!( 8 | CoreGrantType::Implicit, 9 | serde_json::from_str::(&serialized_implicit).unwrap() 10 | ); 11 | } 12 | 13 | #[test] 14 | fn test_signature_alg_serde_plain() { 15 | assert_eq!( 16 | serde_plain::to_string(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256).unwrap(), 17 | "RS256" 18 | ); 19 | assert_eq!( 20 | serde_plain::from_str::("RS256").unwrap(), 21 | CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 David Ramos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/token.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | AdditionalClaims, ExtraTokenFields, GenderClaim, IdToken, IdTokenFields, 3 | JweContentEncryptionAlgorithm, JwsSigningAlgorithm, OAuth2TokenResponse, StandardTokenResponse, 4 | TokenType, 5 | }; 6 | 7 | /// Extends the base OAuth2 token response with an ID token. 8 | pub trait TokenResponse: OAuth2TokenResponse 9 | where 10 | AC: AdditionalClaims, 11 | GC: GenderClaim, 12 | JE: JweContentEncryptionAlgorithm, 13 | JS: JwsSigningAlgorithm, 14 | { 15 | /// Returns the ID token provided by the token response. 16 | /// 17 | /// OpenID Connect authorization servers should always return this field, but it is optional 18 | /// to allow for interoperability with authorization servers that only support OAuth2. 19 | fn id_token(&self) -> Option<&IdToken>; 20 | } 21 | 22 | impl TokenResponse 23 | for StandardTokenResponse, TT> 24 | where 25 | AC: AdditionalClaims, 26 | EF: ExtraTokenFields, 27 | GC: GenderClaim, 28 | JE: JweContentEncryptionAlgorithm, 29 | JS: JwsSigningAlgorithm, 30 | TT: TokenType, 31 | { 32 | fn id_token(&self) -> Option<&IdToken> { 33 | self.extra_fields().id_token() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/http_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::AccessToken; 2 | 3 | use http::header::{HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_TYPE}; 4 | 5 | pub const MIME_TYPE_JSON: &str = "application/json"; 6 | pub const MIME_TYPE_JWKS: &str = "application/jwk-set+json"; 7 | pub const MIME_TYPE_JWT: &str = "application/jwt"; 8 | 9 | pub const BEARER: &str = "Bearer"; 10 | 11 | // The [essence](https://mimesniff.spec.whatwg.org/#mime-type-essence) is the / 12 | // representation. 13 | pub fn content_type_has_essence(content_type: &HeaderValue, expected_essence: &str) -> bool { 14 | #[allow(clippy::or_fun_call)] 15 | content_type 16 | .to_str() 17 | .ok() 18 | .filter(|ct| { 19 | ct[..ct.find(';').unwrap_or(ct.len())].to_lowercase() == expected_essence.to_lowercase() 20 | }) 21 | .is_some() 22 | } 23 | 24 | pub fn check_content_type(headers: &HeaderMap, expected_content_type: &str) -> Result<(), String> { 25 | headers 26 | .get(CONTENT_TYPE) 27 | .map_or(Ok(()), |content_type| 28 | // Section 3.1.1.1 of RFC 7231 indicates that media types are case insensitive and 29 | // may be followed by optional whitespace and/or a parameter (e.g., charset). 30 | // See https://tools.ietf.org/html/rfc7231#section-3.1.1.1. 31 | if !content_type_has_essence(content_type, expected_content_type) { 32 | Err( 33 | format!( 34 | "Unexpected response Content-Type: {:?}, should be `{}`", 35 | content_type, 36 | expected_content_type 37 | ) 38 | ) 39 | } else { 40 | Ok(()) 41 | } 42 | ) 43 | } 44 | 45 | pub fn auth_bearer(access_token: &AccessToken) -> (HeaderName, HeaderValue) { 46 | ( 47 | AUTHORIZATION, 48 | HeaderValue::from_str(&format!("{} {}", BEARER, access_token.secret())) 49 | .expect("invalid access token"), 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "openidconnect" 3 | version = "4.0.1" 4 | authors = ["David A. Ramos "] 5 | description = "OpenID Connect library" 6 | keywords = ["openid", "oidc", "oauth2", "authentication", "auth"] 7 | license = "MIT" 8 | repository = "https://github.com/ramosbugs/openidconnect-rs" 9 | edition = "2021" 10 | readme = "README.md" 11 | rust-version = "1.65" 12 | 13 | [package.metadata.docs.rs] 14 | all-features = true 15 | 16 | [badges] 17 | maintenance = { status = "actively-developed" } 18 | 19 | [features] 20 | accept-rfc3339-timestamps = [] 21 | accept-string-booleans = [] 22 | curl = ["oauth2/curl"] 23 | default = ["reqwest", "rustls-tls"] 24 | native-tls = ["oauth2/native-tls"] 25 | reqwest = ["oauth2/reqwest"] 26 | reqwest-blocking = ["oauth2/reqwest-blocking"] 27 | rustls-tls = ["oauth2/rustls-tls"] 28 | timing-resistant-secret-traits = ["oauth2/timing-resistant-secret-traits"] 29 | ureq = ["oauth2/ureq"] 30 | 31 | [[example]] 32 | name = "gitlab" 33 | required-features = ["reqwest-blocking"] 34 | 35 | [[example]] 36 | name = "google" 37 | required-features = ["reqwest-blocking"] 38 | 39 | [[example]] 40 | name = "okta_device_grant" 41 | required-features = ["reqwest-blocking"] 42 | 43 | [dependencies] 44 | base64 = "0.22" 45 | # Disable 'time' dependency since it triggers RUSTSEC-2020-0071 and we don't need it. 46 | chrono = { version = "0.4", default-features = false, features = [ 47 | "clock", 48 | "std", 49 | "wasmbind" 50 | ] } 51 | thiserror = "1.0" 52 | http = "1.0" 53 | itertools = "0.14" 54 | log = "0.4" 55 | oauth2 = { version = "5.0.0", default-features = false } 56 | rand = "0.8.5" 57 | hmac = "0.12.1" 58 | rsa = "0.9.2" 59 | sha2 = { version = "0.10.6", features = ["oid"] } # Object ID needed for pkcs1v15 padding 60 | p256 = "0.13.2" 61 | p384 = "0.13.0" 62 | dyn-clone = "1.0.10" 63 | serde = "1.0" 64 | serde_json = "1.0" 65 | serde_path_to_error = "0.1" 66 | serde_plain = "1.0" 67 | serde_with = "3" 68 | serde-value = "0.7" 69 | url = { version = "2.4", features = ["serde"] } 70 | subtle = "2.4" 71 | ed25519-dalek = { version = "2.0.0", features = ["pem"] } 72 | 73 | [dev-dependencies] 74 | color-backtrace = { version = "0.5" } 75 | env_logger = "0.9" 76 | pretty_assertions = "1.0" 77 | reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false } 78 | retry = "1.0" 79 | anyhow = "1.0" 80 | -------------------------------------------------------------------------------- /src/types/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::IssuerUrl; 2 | 3 | #[test] 4 | fn test_issuer_url_append() { 5 | assert_eq!( 6 | "http://example.com/.well-known/openid-configuration", 7 | IssuerUrl::new("http://example.com".to_string()) 8 | .unwrap() 9 | .join(".well-known/openid-configuration") 10 | .unwrap() 11 | .to_string() 12 | ); 13 | assert_eq!( 14 | "http://example.com/.well-known/openid-configuration", 15 | IssuerUrl::new("http://example.com/".to_string()) 16 | .unwrap() 17 | .join(".well-known/openid-configuration") 18 | .unwrap() 19 | .to_string() 20 | ); 21 | assert_eq!( 22 | "http://example.com/x/.well-known/openid-configuration", 23 | IssuerUrl::new("http://example.com/x".to_string()) 24 | .unwrap() 25 | .join(".well-known/openid-configuration") 26 | .unwrap() 27 | .to_string() 28 | ); 29 | assert_eq!( 30 | "http://example.com/x/.well-known/openid-configuration", 31 | IssuerUrl::new("http://example.com/x/".to_string()) 32 | .unwrap() 33 | .join(".well-known/openid-configuration") 34 | .unwrap() 35 | .to_string() 36 | ); 37 | } 38 | 39 | #[test] 40 | fn test_url_serialize() { 41 | let issuer_url = 42 | IssuerUrl::new("http://example.com/.well-known/openid-configuration".to_string()).unwrap(); 43 | let serialized_url = serde_json::to_string(&issuer_url).unwrap(); 44 | 45 | assert_eq!( 46 | "\"http://example.com/.well-known/openid-configuration\"", 47 | serialized_url 48 | ); 49 | 50 | let deserialized_url = serde_json::from_str(&serialized_url).unwrap(); 51 | assert_eq!(issuer_url, deserialized_url); 52 | 53 | assert_eq!( 54 | serde_json::to_string(&IssuerUrl::new("http://example.com".to_string()).unwrap()).unwrap(), 55 | "\"http://example.com\"", 56 | ); 57 | } 58 | 59 | #[cfg(feature = "accept-string-booleans")] 60 | #[test] 61 | fn test_string_bool_parse() { 62 | use crate::helpers::Boolean; 63 | 64 | fn test_case(input: &str, expect: bool) { 65 | let value: Boolean = serde_json::from_str(input).unwrap(); 66 | assert_eq!(value.0, expect); 67 | } 68 | test_case("true", true); 69 | test_case("false", false); 70 | test_case("\"true\"", true); 71 | test_case("\"false\"", false); 72 | assert!(serde_json::from_str::("\"maybe\"").is_err()); 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) Library for Rust 2 | 3 | [![crates.io](https://img.shields.io/crates/v/openidconnect.svg)](https://crates.io/crates/openidconnect) 4 | [![docs.rs](https://docs.rs/openidconnect/badge.svg)](https://docs.rs/openidconnect) 5 | [![Build Status](https://github.com/ramosbugs/openidconnect-rs/actions/workflows/main.yml/badge.svg)](https://github.com/ramosbugs/openidconnect-rs/actions/workflows/main.yml) 6 | [![codecov](https://codecov.io/gh/ramosbugs/openidconnect-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/ramosbugs/openidconnect-rs) 7 | 8 | This library provides extensible, strongly-typed interfaces for the OpenID 9 | Connect protocol, which can be used to authenticate users via 10 | [Google](https://developers.google.com/identity/openid-connect/openid-connect), 11 | [GitLab](https://docs.gitlab.com/ee/integration/openid_connect_provider.html), 12 | [Microsoft](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc), 13 | and [many other providers](https://openid.net/certification/#OPENID-OP-P). 14 | 15 | API documentation and examples are available on [docs.rs](https://docs.rs/openidconnect). 16 | 17 | ## Minimum Supported Rust Version (MSRV) 18 | 19 | The MSRV for *3.3* and newer releases of this crate is Rust **1.65**. 20 | 21 | The MSRV for *3.0* to *3.2* releases of this crate is Rust **1.57**. 22 | 23 | The MSRV for *2.x* releases of this crate is Rust 1.45. 24 | 25 | Since the 3.0.0 release, this crate maintains a policy of supporting 26 | Rust releases going back at least 6 months. Changes that break compatibility with Rust releases 27 | older than 6 months will no longer be considered SemVer breaking changes and will not result in a 28 | new major version number for this crate. MSRV changes will coincide with minor version updates 29 | and will not happen in patch releases. 30 | 31 | ## Standards 32 | 33 | * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) 34 | * Supported features: 35 | * Relying Party flows: code, implicit, hybrid 36 | * Standard claims 37 | * UserInfo endpoint 38 | * RSA, HMAC, ECDSA (P-256/P-384 curves) and EdDSA (Ed25519 curve) ID token verification 39 | * Unsupported features: 40 | * Aggregated and distributed claims 41 | * Passing request parameters as JWTs 42 | * Verification of the `azp` claim (see [discussion](https://bitbucket.org/openid/connect/issues/973/)) 43 | * ECDSA-based ID token verification using the P-521 curve 44 | * JSON Web Encryption (JWE) 45 | * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) 46 | * Supported features: 47 | * Provider Metadata 48 | * Unsupported features: 49 | * WebFinger 50 | * [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html) 51 | * Supported features: 52 | * Client Metadata 53 | * Client Registration endpoint 54 | * Unsupported features: 55 | * Client Configuration endpoint 56 | * [OpenID Connect RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html) 57 | * [OAuth 2.0 Token Introspection](https://tools.ietf.org/html/rfc7662) 58 | * [OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009) 59 | * [OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628) 60 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: {} 7 | pull_request: {} 8 | schedule: 9 | # Run daily to catch breakages in new Rust versions as well as new cargo audit findings. 10 | - cron: '0 16 * * *' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | env: 16 | CARGO_TERM_COLOR: always 17 | 18 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 19 | jobs: 20 | # This workflow contains a single job called "build" 21 | test: 22 | # The type of runner that the job will run on 23 | runs-on: ${{ matrix.rust_os.os }} 24 | 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | rust_os: 29 | - { rust: 1.65.0, os: ubuntu-22.04 } 30 | - { rust: stable, os: ubuntu-22.04 } 31 | - { rust: beta, os: ubuntu-22.04 } 32 | - { rust: nightly, os: ubuntu-22.04 } 33 | 34 | env: 35 | CARGO_NET_GIT_FETCH_WITH_CLI: "true" 36 | 37 | # Steps represent a sequence of tasks that will be executed as part of the job 38 | steps: 39 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 40 | - uses: actions/checkout@v2 41 | 42 | - name: Print git branch name 43 | run: git rev-parse --abbrev-ref HEAD 44 | 45 | - run: git show-ref | grep $(git rev-parse HEAD) 46 | 47 | - name: Install Rust toolchain 48 | uses: actions-rs/toolchain@v1 49 | with: 50 | toolchain: ${{ matrix.rust_os.rust }} 51 | override: true 52 | components: clippy, rustfmt 53 | target: wasm32-unknown-unknown 54 | 55 | # Newer dependency versions may not support rustc 1.65, so we use a Cargo.lock file for those 56 | # builds. 57 | - name: Use Rust 1.65 lockfile 58 | if: ${{ matrix.rust_os.rust == '1.65.0' }} 59 | run: | 60 | cp Cargo-1.65.lock Cargo.lock 61 | echo "CARGO_LOCKED=--locked" >> $GITHUB_ENV 62 | 63 | - name: Run tests 64 | run: cargo ${CARGO_LOCKED} test --tests --examples 65 | - name: Doc tests 66 | run: | 67 | cargo ${CARGO_LOCKED} test --doc 68 | cargo ${CARGO_LOCKED} test --doc --no-default-features 69 | cargo ${CARGO_LOCKED} test --doc --all-features 70 | - name: Test with all features enabled 71 | run: cargo ${CARGO_LOCKED} test --all-features 72 | 73 | - name: Check fmt 74 | if: ${{ matrix.rust_os.rust == '1.65.0' }} 75 | run: cargo ${CARGO_LOCKED} fmt --all -- --check 76 | 77 | - name: Clippy 78 | if: ${{ matrix.rust_os.rust == '1.65.0' }} 79 | run: cargo ${CARGO_LOCKED} clippy --all --all-features -- --deny warnings 80 | 81 | - name: Audit 82 | if: ${{ matrix.rust_os.rust == 'stable' }} 83 | run: | 84 | cargo install --force cargo-audit 85 | # The chrono thread safety issue doesn't affect this crate since the crate does not rely 86 | # on the system's local time zone, only UTC. See: 87 | # https://github.com/chronotope/chrono/issues/499#issuecomment-946388161 88 | # FIXME(ramosbugs/openidconnect-rs#140): upgrade `rsa` once fix for RUSTSEC-2023-0071 is 89 | # available. 90 | cargo ${CARGO_LOCKED} audit \ 91 | --ignore RUSTSEC-2020-0159 \ 92 | --ignore RUSTSEC-2023-0071 93 | 94 | - name: Check WASM build 95 | run: cargo ${CARGO_LOCKED} check --target wasm32-unknown-unknown 96 | 97 | coverage: 98 | runs-on: ubuntu-latest 99 | container: 100 | image: xd009642/tarpaulin:0.32.0 101 | options: --security-opt seccomp=unconfined 102 | steps: 103 | - uses: actions/checkout@v2 104 | - name: Generate code coverage 105 | run: | 106 | cargo ${CARGO_LOCKED} tarpaulin --verbose --all-features --timeout 120 --out Xml 107 | - name: Upload to codecov.io 108 | uses: codecov/codecov-action@v3 109 | with: 110 | fail_ci_if_error: false 111 | -------------------------------------------------------------------------------- /tests/rp_certification_dynamic.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | 3 | use crate::rp_common::{ 4 | get_provider_metadata, init_log, issuer_url, register_client, CERTIFICATION_BASE_URL, RP_NAME, 5 | }; 6 | 7 | use log::{debug, info}; 8 | 9 | mod rp_common; 10 | 11 | #[test] 12 | #[ignore] 13 | fn rp_discovery_openid_configuration() { 14 | const TEST_ID: &str = "rp-discovery-openid-configuration"; 15 | init_log(TEST_ID); 16 | 17 | let _issuer_url = issuer_url(TEST_ID); 18 | let provider_metadata = get_provider_metadata(TEST_ID); 19 | 20 | macro_rules! log_field { 21 | ($field:ident) => { 22 | log_container_field!(provider_metadata.$field); 23 | }; 24 | } 25 | 26 | log_info!( 27 | "Successfully retrieved provider metadata from {:?}", 28 | _issuer_url 29 | ); 30 | log_field!(issuer); 31 | log_field!(authorization_endpoint); 32 | log_field!(token_endpoint); 33 | log_field!(userinfo_endpoint); 34 | log_field!(jwks_uri); 35 | log_field!(registration_endpoint); 36 | log_field!(scopes_supported); 37 | log_field!(response_types_supported); 38 | log_field!(response_modes_supported); 39 | log_field!(grant_types_supported); 40 | log_field!(acr_values_supported); 41 | log_field!(subject_types_supported); 42 | log_field!(id_token_signing_alg_values_supported); 43 | log_field!(id_token_encryption_alg_values_supported); 44 | log_field!(id_token_encryption_enc_values_supported); 45 | log_field!(userinfo_signing_alg_values_supported); 46 | log_field!(userinfo_encryption_alg_values_supported); 47 | log_field!(userinfo_encryption_enc_values_supported); 48 | log_field!(request_object_signing_alg_values_supported); 49 | log_field!(request_object_encryption_alg_values_supported); 50 | log_field!(request_object_encryption_enc_values_supported); 51 | log_field!(token_endpoint_auth_methods_supported); 52 | log_field!(token_endpoint_auth_signing_alg_values_supported); 53 | log_field!(display_values_supported); 54 | log_field!(claim_types_supported); 55 | log_field!(claims_supported); 56 | log_field!(service_documentation); 57 | log_field!(claims_locales_supported); 58 | log_field!(ui_locales_supported); 59 | log_field!(claims_parameter_supported); 60 | log_field!(request_parameter_supported); 61 | log_field!(request_uri_parameter_supported); 62 | log_field!(require_request_uri_registration); 63 | log_field!(op_policy_uri); 64 | log_field!(op_tos_uri); 65 | 66 | log_debug!("Provider metadata: {:?}", provider_metadata); 67 | 68 | log_info!("SUCCESS"); 69 | } 70 | 71 | #[test] 72 | #[ignore] 73 | fn rp_registration_dynamic() { 74 | const TEST_ID: &str = "rp-registration-dynamic"; 75 | init_log(TEST_ID); 76 | 77 | let _issuer_url = issuer_url(TEST_ID); 78 | let provider_metadata = get_provider_metadata(TEST_ID); 79 | let registration_response = register_client(&provider_metadata, |reg| reg); 80 | 81 | macro_rules! log_field { 82 | ($field:ident) => { 83 | log_container_field!(registration_response.$field); 84 | }; 85 | } 86 | 87 | log_field!(client_id); 88 | log_field!(client_secret); 89 | log_field!(registration_access_token); 90 | log_field!(registration_client_uri); 91 | log_field!(client_id_issued_at); 92 | log_field!(client_secret_expires_at); 93 | log_field!(redirect_uris); 94 | log_field!(response_types); 95 | log_field!(grant_types); 96 | log_field!(application_type); 97 | log_field!(contacts); 98 | log_field!(client_name); 99 | log_field!(logo_uri); 100 | log_field!(client_uri); 101 | log_field!(policy_uri); 102 | log_field!(tos_uri); 103 | log_field!(jwks_uri); 104 | log_field!(jwks); 105 | log_field!(sector_identifier_uri); 106 | log_field!(subject_type); 107 | log_field!(id_token_signed_response_alg); 108 | log_field!(id_token_encrypted_response_alg); 109 | log_field!(id_token_encrypted_response_enc); 110 | log_field!(userinfo_signed_response_alg); 111 | log_field!(userinfo_encrypted_response_alg); 112 | log_field!(userinfo_encrypted_response_enc); 113 | log_field!(request_object_signing_alg); 114 | log_field!(request_object_encryption_alg); 115 | log_field!(request_object_encryption_enc); 116 | log_field!(token_endpoint_auth_method); 117 | log_field!(token_endpoint_auth_signing_alg); 118 | log_field!(default_max_age); 119 | log_field!(require_auth_time); 120 | log_field!(default_acr_values); 121 | log_field!(initiate_login_uri); 122 | log_field!(request_uris); 123 | 124 | log_debug!("Registration response: {:?}", registration_response); 125 | 126 | assert_eq!( 127 | format!( 128 | "{}/{}/registration?client_id={}", 129 | CERTIFICATION_BASE_URL, 130 | RP_NAME, 131 | **registration_response.client_id() 132 | ), 133 | registration_response 134 | .registration_client_uri() 135 | .unwrap() 136 | .to_string() 137 | ); 138 | 139 | log_info!("SUCCESS"); 140 | } 141 | -------------------------------------------------------------------------------- /examples/okta_device_grant.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! This example showcases the process of using the device grant flow to obtain an ID token from the 3 | //! [Okta](https://developer.okta.com/docs/guides/device-authorization-grant/main/#request-the-device-verification-code) 4 | //! provider. 5 | //! 6 | //! Before running it, you'll need to generate your own 7 | //! [Okta Server](https://developer.okta.com/signup/). 8 | //! 9 | //! In order to run the example call: 10 | //! 11 | //! ```sh 12 | //! CLIENT_ID=xxx CLIENT_SECRET=yyy ISSUER_URL=zzz cargo run --example okta_device_grant 13 | //! ``` 14 | //! 15 | //! ...and follow the instructions. 16 | //! 17 | 18 | use openidconnect::core::{ 19 | CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod, 20 | CoreDeviceAuthorizationResponse, CoreGrantType, CoreJsonWebKey, 21 | CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreResponseMode, 22 | CoreResponseType, CoreSubjectIdentifierType, 23 | }; 24 | use openidconnect::reqwest; 25 | use openidconnect::{ 26 | AdditionalProviderMetadata, AuthType, ClientId, ClientSecret, DeviceAuthorizationUrl, 27 | IssuerUrl, ProviderMetadata, Scope, 28 | }; 29 | use serde::{Deserialize, Serialize}; 30 | 31 | use std::env; 32 | use std::process::exit; 33 | 34 | // Obtain the device_authorization_url from the OIDC metadata provider. 35 | #[derive(Clone, Debug, Deserialize, Serialize)] 36 | struct DeviceEndpointProviderMetadata { 37 | device_authorization_endpoint: DeviceAuthorizationUrl, 38 | } 39 | impl AdditionalProviderMetadata for DeviceEndpointProviderMetadata {} 40 | type DeviceProviderMetadata = ProviderMetadata< 41 | DeviceEndpointProviderMetadata, 42 | CoreAuthDisplay, 43 | CoreClientAuthMethod, 44 | CoreClaimName, 45 | CoreClaimType, 46 | CoreGrantType, 47 | CoreJweContentEncryptionAlgorithm, 48 | CoreJweKeyManagementAlgorithm, 49 | CoreJsonWebKey, 50 | CoreResponseMode, 51 | CoreResponseType, 52 | CoreSubjectIdentifierType, 53 | >; 54 | 55 | fn handle_error(fail: &T, msg: &'static str) { 56 | let mut err_msg = format!("ERROR: {}", msg); 57 | let mut cur_fail: Option<&dyn std::error::Error> = Some(fail); 58 | while let Some(cause) = cur_fail { 59 | err_msg += &format!("\n caused by: {}", cause); 60 | cur_fail = cause.source(); 61 | } 62 | println!("{}", err_msg); 63 | exit(1); 64 | } 65 | 66 | fn main() -> Result<(), anyhow::Error> { 67 | env_logger::init(); 68 | 69 | let client_id = 70 | ClientId::new(env::var("CLIENT_ID").expect("Missing the CLIENT_ID environment variable.")); 71 | let client_secret = ClientSecret::new( 72 | env::var("CLIENT_SECRET").expect("Missing the CLIENT_SECRET environment variable."), 73 | ); 74 | let issuer_url = IssuerUrl::new( 75 | env::var("ISSUER_URL").expect("Missing the ISSUER_URL environment variable."), 76 | ) 77 | .unwrap_or_else(|err| { 78 | handle_error(&err, "Invalid issuer URL"); 79 | unreachable!(); 80 | }); 81 | 82 | let http_client = reqwest::blocking::ClientBuilder::new() 83 | // Following redirects opens the client up to SSRF vulnerabilities. 84 | .redirect(reqwest::redirect::Policy::none()) 85 | .build() 86 | .unwrap_or_else(|err| { 87 | handle_error(&err, "Failed to build HTTP client"); 88 | unreachable!(); 89 | }); 90 | 91 | // Fetch Okta's OpenID Connect discovery document. 92 | let provider_metadata = DeviceProviderMetadata::discover(&issuer_url, &http_client) 93 | .unwrap_or_else(|err| { 94 | handle_error(&err, "Failed to discover OpenID Provider"); 95 | unreachable!(); 96 | }); 97 | 98 | // Use the custom metadata to get the device_authorization_endpoint 99 | let device_authorization_endpoint = provider_metadata 100 | .additional_metadata() 101 | .device_authorization_endpoint 102 | .clone(); 103 | 104 | // Set up the config for the Okta device authorization process. 105 | let client = 106 | CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) 107 | .set_device_authorization_url(device_authorization_endpoint) 108 | .set_auth_type(AuthType::RequestBody); 109 | 110 | let details: CoreDeviceAuthorizationResponse = client 111 | .exchange_device_code() 112 | .add_scope(Scope::new("profile".to_string())) 113 | .request(&http_client) 114 | .unwrap_or_else(|err| { 115 | handle_error(&err, "Failed to get device code"); 116 | unreachable!(); 117 | }); 118 | println!("Fetching device code..."); 119 | dbg!(&details); 120 | 121 | // Display the URL and user-code. 122 | println!( 123 | "Open this URL in your browser:\n{}\nand enter the code: {}", 124 | details.verification_uri_complete().unwrap().secret(), 125 | details.user_code().secret() 126 | ); 127 | 128 | // Now poll for the token 129 | let token = client 130 | .exchange_device_access_token(&details) 131 | .unwrap_or_else(|err| { 132 | handle_error(&err, "Failed to get access token"); 133 | unreachable!(); 134 | }) 135 | .request(&http_client, std::thread::sleep, None) 136 | .unwrap_or_else(|err| { 137 | handle_error(&err, "Failed to get access token"); 138 | unreachable!(); 139 | }); 140 | 141 | // Finally, display the ID Token to verify we are using OIDC 142 | println!("ID Token response: {:?}", token.extra_fields().id_token()); 143 | 144 | Ok(()) 145 | } 146 | -------------------------------------------------------------------------------- /src/types/jwk.rs: -------------------------------------------------------------------------------- 1 | use crate::{SignatureVerificationError, SigningError}; 2 | 3 | use serde::de::DeserializeOwned; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use std::fmt::Debug; 7 | use std::hash::Hash; 8 | 9 | new_type![ 10 | /// ID of a JSON Web Key. 11 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 12 | JsonWebKeyId(String) 13 | ]; 14 | 15 | /// JSON Web Key. 16 | pub trait JsonWebKey: Clone + Debug + DeserializeOwned + Serialize + 'static { 17 | /// Allowed key usage. 18 | type KeyUse: JsonWebKeyUse; 19 | 20 | /// JSON Web Signature (JWS) algorithm. 21 | type SigningAlgorithm: JwsSigningAlgorithm; 22 | 23 | /// Returns the key ID, or `None` if no key ID is specified. 24 | fn key_id(&self) -> Option<&JsonWebKeyId>; 25 | 26 | /// Returns the key type (e.g., RSA). 27 | fn key_type(&self) -> &::KeyType; 28 | 29 | /// Returns the allowed key usage (e.g., signing or encryption), or `None` if no usage is 30 | /// specified. 31 | fn key_use(&self) -> Option<&Self::KeyUse>; 32 | 33 | /// Returns the algorithm (e.g. ES512) this key must be used with, or `Unspecified` if 34 | /// no algorithm constraint was given, or unsupported if the algorithm is not for signing. 35 | /// 36 | /// It's not sufficient to tell whether a key can be used for signing, as key use also has to be validated. 37 | fn signing_alg(&self) -> JsonWebKeyAlgorithm<&Self::SigningAlgorithm>; 38 | 39 | /// Initializes a new symmetric key or shared signing secret from the specified raw bytes. 40 | fn new_symmetric(key: Vec) -> Self; 41 | 42 | /// Verifies the given `signature` using the given signature algorithm (`signature_alg`) over 43 | /// the given `message`. 44 | /// 45 | /// Returns `Ok` if the signature is valid, or an `Err` otherwise. 46 | fn verify_signature( 47 | &self, 48 | signature_alg: &Self::SigningAlgorithm, 49 | message: &[u8], 50 | signature: &[u8], 51 | ) -> Result<(), SignatureVerificationError>; 52 | 53 | /// Hashes the given `bytes` using the hash function associated with the specified signing 54 | /// algorithm and returns the hashed bytes. 55 | /// 56 | /// Certain signing algorithms (e.g., `EdDSA`) use different hash functions depending on the 57 | /// type of key (e.g., whether the `Ed25519` or `Ed448` curve is used), so this method is 58 | /// implemented on the corresponding public key instead of the [`JwsSigningAlgorithm`] trait 59 | /// to allow the implementation to determine the proper hash function to use. 60 | /// If hashing fails or this key/signing algorithm does not have an associated hash function, an 61 | /// `Err` is returned with a string describing the cause of the error. An error is also returned 62 | /// if the specified signature algorithm is incompatible with this key (e.g., passing `EdDSA` 63 | /// with an RSA key). 64 | fn hash_bytes(&self, bytes: &[u8], alg: &Self::SigningAlgorithm) -> Result, String>; 65 | } 66 | 67 | /// Encodes a JWK key's alg field compatibility with either signing or encryption operations. 68 | #[derive(Debug)] 69 | pub enum JsonWebKeyAlgorithm { 70 | /// the alg field allows this kind of operation to be performed with this algorithm only 71 | Algorithm(A), 72 | /// there is no alg field 73 | Unspecified, 74 | /// the alg field's algorithm is incompatible with this kind of operation 75 | Unsupported, 76 | } 77 | 78 | /// Private or symmetric key for signing. 79 | pub trait PrivateSigningKey { 80 | /// Corresponding type of JSON Web Key used for verifying signatures produced by this key. 81 | type VerificationKey: JsonWebKey; 82 | 83 | /// Signs the given `message` using the given signature algorithm. 84 | fn sign( 85 | &self, 86 | signature_alg: &::SigningAlgorithm, 87 | message: &[u8], 88 | ) -> Result, SigningError>; 89 | 90 | /// Converts this key to a JSON Web Key that can be used for verifying signatures. 91 | fn as_verification_key(&self) -> Self::VerificationKey; 92 | } 93 | 94 | /// Key type (e.g., RSA). 95 | pub trait JsonWebKeyType: 96 | Clone + Debug + DeserializeOwned + PartialEq + Serialize + 'static 97 | { 98 | } 99 | 100 | /// Allowed key usage. 101 | pub trait JsonWebKeyUse: Debug + DeserializeOwned + Serialize + 'static { 102 | /// Returns true if the associated key may be used for digital signatures, or false otherwise. 103 | fn allows_signature(&self) -> bool; 104 | 105 | /// Returns true if the associated key may be used for encryption, or false otherwise. 106 | fn allows_encryption(&self) -> bool; 107 | } 108 | 109 | /// JSON Web Encryption (JWE) content encryption algorithm. 110 | pub trait JweContentEncryptionAlgorithm: 111 | Clone + Debug + DeserializeOwned + Serialize + 'static 112 | { 113 | /// Key type (e.g., RSA). 114 | type KeyType: JsonWebKeyType; 115 | 116 | /// Returns the type of key required to use this encryption algorithm. 117 | fn key_type(&self) -> Result; 118 | } 119 | 120 | /// JSON Web Encryption (JWE) key management algorithm. 121 | pub trait JweKeyManagementAlgorithm: Debug + DeserializeOwned + Serialize + 'static { 122 | // TODO: add a key_type() method 123 | } 124 | 125 | /// JSON Web Signature (JWS) algorithm. 126 | pub trait JwsSigningAlgorithm: 127 | Clone + Debug + DeserializeOwned + Eq + Hash + PartialEq + Serialize + 'static 128 | { 129 | /// Key type (e.g., RSA). 130 | type KeyType: JsonWebKeyType; 131 | 132 | /// Returns the type of key required to use this signature algorithm, or `None` if this 133 | /// algorithm does not require a key. 134 | fn key_type(&self) -> Option; 135 | 136 | /// Returns true if the signature algorithm uses a shared secret (symmetric key). 137 | fn uses_shared_secret(&self) -> bool; 138 | 139 | /// Returns the RS256 algorithm. 140 | /// 141 | /// This is the default algorithm for OpenID Connect ID tokens and must be supported by all 142 | /// implementations. 143 | fn rsa_sha_256() -> Self; 144 | } 145 | -------------------------------------------------------------------------------- /src/types/localized.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use std::borrow::Cow; 4 | use std::collections::HashMap; 5 | use std::fmt::Display; 6 | 7 | new_type![ 8 | /// Language tag adhering to RFC 5646 (e.g., `fr` or `fr-CA`). 9 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 10 | LanguageTag(String) 11 | ]; 12 | impl AsRef for LanguageTag { 13 | fn as_ref(&self) -> &str { 14 | self 15 | } 16 | } 17 | impl Display for LanguageTag { 18 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 19 | write!(f, "{}", self.as_ref()) 20 | } 21 | } 22 | 23 | pub(crate) fn split_language_tag_key(key: &str) -> (&str, Option) { 24 | let mut lang_tag_sep = key.splitn(2, '#'); 25 | 26 | // String::splitn(2) always returns at least one element. 27 | let field_name = lang_tag_sep.next().unwrap(); 28 | 29 | let language_tag = lang_tag_sep 30 | .next() 31 | .filter(|language_tag| !language_tag.is_empty()) 32 | .map(|language_tag| LanguageTag::new(language_tag.to_string())); 33 | 34 | (field_name, language_tag) 35 | } 36 | 37 | pub(crate) fn join_language_tag_key<'a>( 38 | field_name: &'a str, 39 | language_tag: Option<&LanguageTag>, 40 | ) -> Cow<'a, str> { 41 | if let Some(language_tag) = language_tag { 42 | Cow::Owned(format!("{field_name}#{language_tag}")) 43 | } else { 44 | Cow::Borrowed(field_name) 45 | } 46 | } 47 | 48 | /// A [locale-aware](https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsLanguages) 49 | /// claim. 50 | /// 51 | /// This structure associates one more `Option` locales with the corresponding 52 | /// claims values. 53 | #[derive(Clone, Debug, PartialEq, Eq)] 54 | pub struct LocalizedClaim(HashMap, Option); 55 | impl LocalizedClaim { 56 | /// Initialize an empty claim. 57 | pub fn new() -> Self { 58 | Self::default() 59 | } 60 | 61 | /// Returns true if the claim contains a value for the specified locale. 62 | pub fn contains_key(&self, locale: Option<&LanguageTag>) -> bool { 63 | if let Some(l) = locale { 64 | self.0.contains_key(l) 65 | } else { 66 | self.1.is_some() 67 | } 68 | } 69 | 70 | /// Returns the entry for the specified locale or `None` if there is no such entry. 71 | pub fn get(&self, locale: Option<&LanguageTag>) -> Option<&T> { 72 | if let Some(l) = locale { 73 | self.0.get(l) 74 | } else { 75 | self.1.as_ref() 76 | } 77 | } 78 | 79 | /// Returns an iterator over the locales and claim value entries. 80 | pub fn iter(&self) -> impl Iterator, &T)> { 81 | self.1 82 | .iter() 83 | .map(|value| (None, value)) 84 | .chain(self.0.iter().map(|(locale, value)| (Some(locale), value))) 85 | } 86 | 87 | /// Inserts or updates an entry for the specified locale. 88 | /// 89 | /// Returns the current value associated with the given locale, or `None` if there is no 90 | /// such entry. 91 | pub fn insert(&mut self, locale: Option, value: T) -> Option { 92 | if let Some(l) = locale { 93 | self.0.insert(l, value) 94 | } else { 95 | self.1.replace(value) 96 | } 97 | } 98 | 99 | /// Removes an entry for the specified locale. 100 | /// 101 | /// Returns the current value associated with the given locale, or `None` if there is no 102 | /// such entry. 103 | pub fn remove(&mut self, locale: Option<&LanguageTag>) -> Option { 104 | if let Some(l) = locale { 105 | self.0.remove(l) 106 | } else { 107 | self.1.take() 108 | } 109 | } 110 | } 111 | impl LocalizedClaim> { 112 | pub(crate) fn flatten_or_none(self) -> Option> { 113 | let flattened_tagged = self 114 | .0 115 | .into_iter() 116 | .filter_map(|(k, v)| v.map(|v| (k, v))) 117 | .collect::>(); 118 | let flattened_default = self.1.flatten(); 119 | 120 | if flattened_tagged.is_empty() && flattened_default.is_none() { 121 | None 122 | } else { 123 | Some(LocalizedClaim(flattened_tagged, flattened_default)) 124 | } 125 | } 126 | } 127 | 128 | impl Default for LocalizedClaim { 129 | fn default() -> Self { 130 | Self(HashMap::new(), None) 131 | } 132 | } 133 | impl From for LocalizedClaim { 134 | fn from(default: T) -> Self { 135 | Self(HashMap::new(), Some(default)) 136 | } 137 | } 138 | impl FromIterator<(Option, T)> for LocalizedClaim { 139 | fn from_iter, T)>>(iter: I) -> Self { 140 | let mut temp: HashMap, T> = iter.into_iter().collect(); 141 | let default = temp.remove(&None); 142 | Self( 143 | temp.into_iter() 144 | .filter_map(|(locale, value)| locale.map(|l| (l, value))) 145 | .collect(), 146 | default, 147 | ) 148 | } 149 | } 150 | impl IntoIterator for LocalizedClaim 151 | where 152 | T: 'static, 153 | { 154 | type Item = as Iterator>::Item; 155 | type IntoIter = LocalizedClaimIterator; 156 | 157 | fn into_iter(self) -> Self::IntoIter { 158 | LocalizedClaimIterator { 159 | inner: Box::new( 160 | self.1.into_iter().map(|value| (None, value)).chain( 161 | self.0 162 | .into_iter() 163 | .map(|(locale, value)| (Some(locale), value)), 164 | ), 165 | ), 166 | } 167 | } 168 | } 169 | 170 | /// Owned iterator over a LocalizedClaim. 171 | pub struct LocalizedClaimIterator { 172 | inner: Box, T)>>, 173 | } 174 | impl Iterator for LocalizedClaimIterator { 175 | type Item = (Option, T); 176 | fn next(&mut self) -> Option { 177 | self.inner.next() 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /tests/rp_common.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity, clippy::expect_fun_call)] 2 | 3 | use log::{error, warn}; 4 | use openidconnect::core::{ 5 | CoreApplicationType, CoreClientRegistrationRequest, CoreClientRegistrationResponse, 6 | CoreProviderMetadata, 7 | }; 8 | use openidconnect::{ 9 | ClientContactEmail, ClientName, HttpClientError, HttpRequest, HttpResponse, IssuerUrl, 10 | RedirectUrl, 11 | }; 12 | 13 | use std::cell::RefCell; 14 | use std::sync::Once; 15 | use std::time::Duration; 16 | 17 | pub const CERTIFICATION_BASE_URL: &str = "https://rp.certification.openid.net:8080"; 18 | pub const RP_CONTACT_EMAIL: &str = "ramos@cs.stanford.edu"; 19 | pub const RP_NAME: &str = "openidconnect-rs"; 20 | pub const RP_REDIRECT_URI: &str = "http://localhost:8080"; 21 | 22 | static INIT_LOG: Once = Once::new(); 23 | 24 | thread_local! { 25 | static TEST_ID: RefCell<&'static str> = const { RefCell::new("UNINITIALIZED_TEST_ID") }; 26 | } 27 | 28 | pub fn get_test_id() -> &'static str { 29 | TEST_ID.with(|id| *id.borrow()) 30 | } 31 | 32 | pub fn set_test_id(test_id: &'static str) { 33 | TEST_ID.with(|id| *id.borrow_mut() = test_id); 34 | } 35 | 36 | #[macro_export] 37 | macro_rules! log_error { 38 | ($($args:tt)+) => { 39 | error!("[{}] {}", rp_common::get_test_id(), format!($($args)+)) 40 | } 41 | } 42 | #[macro_export] 43 | macro_rules! log_info { 44 | ($($args:tt)+) => { 45 | info!("[{}] {}", rp_common::get_test_id(), format!($($args)+)); 46 | } 47 | } 48 | #[macro_export] 49 | macro_rules! log_debug { 50 | ($($args:tt)+) => { 51 | debug!("[{}] {}", rp_common::get_test_id(), format!($($args)+)); 52 | } 53 | } 54 | 55 | #[macro_export] 56 | macro_rules! log_container_field { 57 | ($container:ident. $field:ident) => { 58 | log_info!( 59 | concat!(" ", stringify!($field), " = {:?}"), 60 | $container.$field() 61 | ); 62 | }; 63 | } 64 | 65 | fn _init_log() { 66 | color_backtrace::install(); 67 | env_logger::init(); 68 | } 69 | 70 | pub fn init_log(test_id: &'static str) { 71 | INIT_LOG.call_once(_init_log); 72 | set_test_id(test_id); 73 | } 74 | 75 | // FIXME: just clone `request` directly once we update `http` to 1.0, which implements `Clone`. 76 | #[cfg(feature = "reqwest-blocking")] 77 | pub(crate) fn clone_request(request: &HttpRequest) -> HttpRequest { 78 | let mut request_copy = http::Request::builder() 79 | .method(request.method().to_owned()) 80 | .uri(request.uri().to_owned()) 81 | .version(request.version()); 82 | 83 | for (name, value) in request.headers() { 84 | request_copy = request_copy.header(name, value); 85 | } 86 | request_copy.body(request.body().to_owned()).unwrap() 87 | } 88 | 89 | pub fn http_client(request: HttpRequest) -> Result> { 90 | retry::retry( 91 | (0..5).map(|i| { 92 | if i != 0 { 93 | warn!("Retrying HTTP request ({}/5)", i + 1) 94 | } 95 | Duration::from_millis(500) 96 | }), 97 | || -> Result> { 98 | #[cfg(feature = "reqwest-blocking")] 99 | { 100 | use openidconnect::SyncHttpClient; 101 | reqwest::blocking::Client::default().call(clone_request(&request)) 102 | } 103 | #[cfg(not(feature = "reqwest-blocking"))] 104 | { 105 | let _ = &request; 106 | panic!("reqwest-blocking feature is required") 107 | } 108 | }, 109 | ) 110 | .map_err(|err| match err { 111 | retry::Error::Operation { error, .. } => error, 112 | retry::Error::Internal(msg) => panic!("unexpected error: {msg}"), 113 | }) 114 | } 115 | 116 | pub trait PanicIfFail 117 | where 118 | F: std::error::Error, 119 | { 120 | fn panic_if_fail(self, msg: &'static str) -> T; 121 | } 122 | impl PanicIfFail for Result 123 | where 124 | F: std::error::Error, 125 | { 126 | fn panic_if_fail(self, msg: &'static str) -> T { 127 | match self { 128 | Ok(ret) => ret, 129 | Err(fail) => { 130 | let mut err_msg = format!("Panic: {}", msg); 131 | 132 | let mut cur_fail: Option<&dyn std::error::Error> = Some(&fail); 133 | while let Some(cause) = cur_fail { 134 | err_msg += &format!("\n caused by: {}", cause); 135 | cur_fail = cause.source(); 136 | } 137 | error!("[{}] {}", get_test_id(), err_msg); 138 | panic!("{}", msg); 139 | } 140 | } 141 | } 142 | } 143 | 144 | pub fn issuer_url(test_id: &str) -> IssuerUrl { 145 | IssuerUrl::new(format!( 146 | "{}/{}/{}", 147 | CERTIFICATION_BASE_URL, RP_NAME, test_id 148 | )) 149 | .expect("Failed to parse issuer URL") 150 | } 151 | 152 | pub fn get_provider_metadata(test_id: &str) -> CoreProviderMetadata { 153 | let _issuer_url = issuer_url(test_id); 154 | CoreProviderMetadata::discover(&_issuer_url, &http_client).expect(&format!( 155 | "Failed to fetch provider metadata from {:?}", 156 | _issuer_url 157 | )) 158 | } 159 | 160 | pub fn register_client( 161 | provider_metadata: &CoreProviderMetadata, 162 | request_fn: F, 163 | ) -> CoreClientRegistrationResponse 164 | where 165 | F: FnOnce(CoreClientRegistrationRequest) -> CoreClientRegistrationRequest, 166 | { 167 | let registration_request_pre = CoreClientRegistrationRequest::new( 168 | vec![RedirectUrl::new(RP_REDIRECT_URI.to_string()).unwrap()], 169 | Default::default(), 170 | ) 171 | .set_application_type(Some(CoreApplicationType::Native)) 172 | .set_client_name(Some( 173 | vec![(None, ClientName::new(RP_NAME.to_string()))] 174 | .into_iter() 175 | .collect(), 176 | )) 177 | .set_contacts(Some(vec![ClientContactEmail::new( 178 | RP_CONTACT_EMAIL.to_string(), 179 | )])); 180 | 181 | let registration_request_post = request_fn(registration_request_pre); 182 | 183 | let registration_endpoint = provider_metadata 184 | .registration_endpoint() 185 | .expect("provider does not support dynamic registration"); 186 | registration_request_post 187 | .register(registration_endpoint, &http_client) 188 | .expect(&format!( 189 | "Failed to register client at {:?}", 190 | registration_endpoint 191 | )) 192 | } 193 | -------------------------------------------------------------------------------- /src/types/jwks.rs: -------------------------------------------------------------------------------- 1 | use crate::http_utils::{check_content_type, MIME_TYPE_JSON, MIME_TYPE_JWKS}; 2 | use crate::types::jwk::{JsonWebKey, JsonWebKeyId, JwsSigningAlgorithm}; 3 | use crate::{ 4 | AsyncHttpClient, DiscoveryError, HttpRequest, HttpResponse, JsonWebKeyUse, SyncHttpClient, 5 | }; 6 | 7 | use http::header::ACCEPT; 8 | use http::{HeaderValue, Method, StatusCode}; 9 | use serde::{Deserialize, Serialize}; 10 | use serde_with::{serde_as, VecSkipError}; 11 | 12 | use std::future::Future; 13 | 14 | new_url_type![ 15 | /// JSON Web Key Set URL. 16 | JsonWebKeySetUrl 17 | ]; 18 | 19 | /// JSON Web Key Set. 20 | #[serde_as] 21 | #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] 22 | pub struct JsonWebKeySet 23 | where 24 | K: JsonWebKey, 25 | { 26 | // FIXME: write a test that ensures duplicate object member names cause an error 27 | // (see https://tools.ietf.org/html/rfc7517#section-5) 28 | #[serde(bound = "K: JsonWebKey")] 29 | // Ignores invalid keys rather than failing. That way, clients can function using the keys that 30 | // they do understand, which is fine if they only ever get JWTs signed with those keys. 31 | #[serde_as(as = "VecSkipError<_>")] 32 | keys: Vec, 33 | } 34 | 35 | /// Checks whether a JWK key can be used with a given signing algorithm. 36 | pub(crate) fn check_key_compatibility( 37 | key: &K, 38 | signing_algorithm: &K::SigningAlgorithm, 39 | ) -> Result<(), &'static str> 40 | where 41 | K: JsonWebKey, 42 | { 43 | // if this key isn't suitable for signing 44 | if let Some(use_) = key.key_use() { 45 | if !use_.allows_signature() { 46 | return Err("key usage not permitted for digital signatures"); 47 | } 48 | } 49 | 50 | // if this key doesn't have the right key type 51 | if signing_algorithm.key_type().as_ref() != Some(key.key_type()) { 52 | return Err("key type does not match signature algorithm"); 53 | } 54 | 55 | match key.signing_alg() { 56 | // if no specific algorithm is mandated, any will do 57 | crate::JsonWebKeyAlgorithm::Unspecified => Ok(()), 58 | crate::JsonWebKeyAlgorithm::Unsupported => Err("key algorithm is not a signing algorithm"), 59 | crate::JsonWebKeyAlgorithm::Algorithm(key_alg) if key_alg == signing_algorithm => Ok(()), 60 | crate::JsonWebKeyAlgorithm::Algorithm(_) => Err("incompatible key algorithm"), 61 | } 62 | } 63 | 64 | impl JsonWebKeySet 65 | where 66 | K: JsonWebKey, 67 | { 68 | /// Create a new JSON Web Key Set. 69 | pub fn new(keys: Vec) -> Self { 70 | Self { keys } 71 | } 72 | 73 | /// Return a list of suitable keys, given a key ID and signature algorithm 74 | pub(crate) fn filter_keys( 75 | &self, 76 | key_id: Option<&JsonWebKeyId>, 77 | signature_alg: &K::SigningAlgorithm, 78 | ) -> Vec<&K> { 79 | self.keys() 80 | .iter() 81 | .filter(|key| 82 | // Either the JWT doesn't include a 'kid' (in which case any 'kid' 83 | // is acceptable), or the 'kid' matches the key's ID. 84 | if key_id.is_some() && key_id != key.key_id() { 85 | false 86 | } else { 87 | check_key_compatibility(*key, signature_alg).is_ok() 88 | } 89 | ) 90 | .collect() 91 | } 92 | 93 | /// Fetch a remote JSON Web Key Set from the specified `url` using the given `http_client` 94 | /// (e.g., [`reqwest::blocking::Client`](crate::reqwest::blocking::Client) or 95 | /// [`CurlHttpClient`](crate::CurlHttpClient)). 96 | pub fn fetch( 97 | url: &JsonWebKeySetUrl, 98 | http_client: &C, 99 | ) -> Result::Error>> 100 | where 101 | C: SyncHttpClient, 102 | { 103 | http_client 104 | .call(Self::fetch_request(url).map_err(|err| { 105 | DiscoveryError::Other(format!("failed to prepare request: {err}")) 106 | })?) 107 | .map_err(DiscoveryError::Request) 108 | .and_then(Self::fetch_response) 109 | } 110 | 111 | /// Fetch a remote JSON Web Key Set from the specified `url` using the given async `http_client` 112 | /// (e.g., [`reqwest::Client`](crate::reqwest::Client)). 113 | pub fn fetch_async<'c, C>( 114 | url: &JsonWebKeySetUrl, 115 | http_client: &'c C, 116 | ) -> impl Future>::Error>>> + 'c 117 | where 118 | Self: 'c, 119 | C: AsyncHttpClient<'c>, 120 | { 121 | let fetch_request = Self::fetch_request(url) 122 | .map_err(|err| DiscoveryError::Other(format!("failed to prepare request: {err}"))); 123 | Box::pin(async move { 124 | http_client 125 | .call(fetch_request?) 126 | .await 127 | .map_err(DiscoveryError::Request) 128 | .and_then(Self::fetch_response) 129 | }) 130 | } 131 | 132 | fn fetch_request(url: &JsonWebKeySetUrl) -> Result { 133 | http::Request::builder() 134 | .uri(url.to_string()) 135 | .method(Method::GET) 136 | .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) 137 | .body(Vec::new()) 138 | } 139 | 140 | fn fetch_response(http_response: HttpResponse) -> Result> 141 | where 142 | RE: std::error::Error + 'static, 143 | { 144 | if http_response.status() != StatusCode::OK { 145 | return Err(DiscoveryError::Response( 146 | http_response.status(), 147 | http_response.body().to_owned(), 148 | format!("HTTP status code {}", http_response.status()), 149 | )); 150 | } 151 | 152 | check_content_type(http_response.headers(), MIME_TYPE_JSON) 153 | .or_else(|err| { 154 | check_content_type(http_response.headers(), MIME_TYPE_JWKS).map_err(|_| err) 155 | }) 156 | .map_err(|err_msg| { 157 | DiscoveryError::Response( 158 | http_response.status(), 159 | http_response.body().to_owned(), 160 | err_msg, 161 | ) 162 | })?; 163 | 164 | serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice( 165 | http_response.body(), 166 | )) 167 | .map_err(DiscoveryError::Parse) 168 | } 169 | 170 | /// Return the keys in this JSON Web Key Set. 171 | pub fn keys(&self) -> &Vec { 172 | &self.keys 173 | } 174 | } 175 | impl Clone for JsonWebKeySet 176 | where 177 | K: JsonWebKey, 178 | { 179 | fn clone(&self) -> Self { 180 | Self::new(self.keys.clone()) 181 | } 182 | } 183 | impl Default for JsonWebKeySet 184 | where 185 | K: JsonWebKey, 186 | { 187 | fn default() -> Self { 188 | Self::new(Vec::new()) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /examples/gitlab.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! This example showcases the process of integrating with the 3 | //! [GitLab OpenID Connect](https://docs.gitlab.com/ee/integration/openid_connect_provider.html) 4 | //! provider. 5 | //! 6 | //! Before running it, you'll need to generate your own 7 | //! [GitLab Application](https://docs.gitlab.com/ee/integration/oauth_provider.html). 8 | //! The application needs `openid`, `profile` and `email` permission. 9 | //! 10 | //! In order to run the example call: 11 | //! 12 | //! ```sh 13 | //! GITLAB_CLIENT_ID=xxx GITLAB_CLIENT_SECRET=yyy cargo run --example gitlab 14 | //! ``` 15 | //! 16 | //! ...and follow the instructions. 17 | //! 18 | 19 | use openidconnect::core::{ 20 | CoreClient, CoreGenderClaim, CoreIdTokenClaims, CoreIdTokenVerifier, CoreProviderMetadata, 21 | CoreResponseType, 22 | }; 23 | use openidconnect::reqwest; 24 | use openidconnect::{AdditionalClaims, UserInfoClaims}; 25 | use openidconnect::{ 26 | AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, 27 | OAuth2TokenResponse, RedirectUrl, Scope, 28 | }; 29 | use serde::{Deserialize, Serialize}; 30 | use url::Url; 31 | 32 | use std::env; 33 | use std::io::{BufRead, BufReader, Write}; 34 | use std::net::TcpListener; 35 | use std::process::exit; 36 | 37 | #[derive(Debug, Deserialize, Serialize)] 38 | struct GitLabClaims { 39 | // Deprecated and thus optional as it might be removed in the futre 40 | sub_legacy: Option, 41 | groups: Vec, 42 | } 43 | impl AdditionalClaims for GitLabClaims {} 44 | 45 | fn handle_error(fail: &T, msg: &'static str) { 46 | let mut err_msg = format!("ERROR: {}", msg); 47 | let mut cur_fail: Option<&dyn std::error::Error> = Some(fail); 48 | while let Some(cause) = cur_fail { 49 | err_msg += &format!("\n caused by: {}", cause); 50 | cur_fail = cause.source(); 51 | } 52 | println!("{}", err_msg); 53 | exit(1); 54 | } 55 | 56 | fn main() { 57 | env_logger::init(); 58 | 59 | let gitlab_client_id = ClientId::new( 60 | env::var("GITLAB_CLIENT_ID").expect("Missing the GITLAB_CLIENT_ID environment variable."), 61 | ); 62 | let gitlab_client_secret = ClientSecret::new( 63 | env::var("GITLAB_CLIENT_SECRET") 64 | .expect("Missing the GITLAB_CLIENT_SECRET environment variable."), 65 | ); 66 | let issuer_url = IssuerUrl::new("https://gitlab.com".to_string()).unwrap_or_else(|err| { 67 | handle_error(&err, "Invalid issuer URL"); 68 | unreachable!(); 69 | }); 70 | 71 | let http_client = reqwest::blocking::ClientBuilder::new() 72 | // Following redirects opens the client up to SSRF vulnerabilities. 73 | .redirect(reqwest::redirect::Policy::none()) 74 | .build() 75 | .unwrap_or_else(|err| { 76 | handle_error(&err, "Failed to build HTTP client"); 77 | unreachable!(); 78 | }); 79 | 80 | // Fetch GitLab's OpenID Connect discovery document. 81 | let provider_metadata = CoreProviderMetadata::discover(&issuer_url, &http_client) 82 | .unwrap_or_else(|err| { 83 | handle_error(&err, "Failed to discover OpenID Provider"); 84 | unreachable!(); 85 | }); 86 | 87 | // Set up the config for the GitLab OAuth2 process. 88 | let client = CoreClient::from_provider_metadata( 89 | provider_metadata, 90 | gitlab_client_id, 91 | Some(gitlab_client_secret), 92 | ) 93 | // This example will be running its own server at localhost:8080. 94 | // See below for the server implementation. 95 | .set_redirect_uri( 96 | RedirectUrl::new("http://localhost:8080".to_string()).unwrap_or_else(|err| { 97 | handle_error(&err, "Invalid redirect URL"); 98 | unreachable!(); 99 | }), 100 | ); 101 | 102 | // Generate the authorization URL to which we'll redirect the user. 103 | let (authorize_url, csrf_state, nonce) = client 104 | .authorize_url( 105 | AuthenticationFlow::::AuthorizationCode, 106 | CsrfToken::new_random, 107 | Nonce::new_random, 108 | ) 109 | // This example is requesting access to the the user's profile including email. 110 | .add_scope(Scope::new("email".to_string())) 111 | .add_scope(Scope::new("profile".to_string())) 112 | .url(); 113 | 114 | println!("Open this URL in your browser:\n{authorize_url}\n"); 115 | 116 | let (code, state) = { 117 | // A very naive implementation of the redirect server. 118 | let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); 119 | 120 | // Accept one connection 121 | let (mut stream, _) = listener.accept().unwrap(); 122 | 123 | let mut reader = BufReader::new(&stream); 124 | 125 | let mut request_line = String::new(); 126 | reader.read_line(&mut request_line).unwrap(); 127 | 128 | let redirect_url = request_line.split_whitespace().nth(1).unwrap(); 129 | let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); 130 | 131 | let code = url 132 | .query_pairs() 133 | .find(|(key, _)| key == "code") 134 | .map(|(_, code)| AuthorizationCode::new(code.into_owned())) 135 | .unwrap(); 136 | 137 | let state = url 138 | .query_pairs() 139 | .find(|(key, _)| key == "state") 140 | .map(|(_, state)| CsrfToken::new(state.into_owned())) 141 | .unwrap(); 142 | 143 | let message = "Go back to your terminal :)"; 144 | let response = format!( 145 | "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", 146 | message.len(), 147 | message 148 | ); 149 | stream.write_all(response.as_bytes()).unwrap(); 150 | 151 | (code, state) 152 | }; 153 | 154 | println!("GitLab returned the following code:\n{}\n", code.secret()); 155 | println!( 156 | "GitLab returned the following state:\n{} (expected `{}`)\n", 157 | state.secret(), 158 | csrf_state.secret() 159 | ); 160 | 161 | // Exchange the code with a token. 162 | let token_response = client 163 | .exchange_code(code) 164 | .unwrap_or_else(|err| { 165 | handle_error(&err, "No user info endpoint"); 166 | unreachable!(); 167 | }) 168 | .request(&http_client) 169 | .unwrap_or_else(|err| { 170 | handle_error(&err, "Failed to contact token endpoint"); 171 | unreachable!(); 172 | }); 173 | 174 | println!( 175 | "GitLab returned access token:\n{}\n", 176 | token_response.access_token().secret() 177 | ); 178 | println!("GitLab returned scopes: {:?}", token_response.scopes()); 179 | 180 | let id_token_verifier: CoreIdTokenVerifier = client.id_token_verifier(); 181 | let id_token_claims: &CoreIdTokenClaims = token_response 182 | .extra_fields() 183 | .id_token() 184 | .expect("Server did not return an ID token") 185 | .claims(&id_token_verifier, &nonce) 186 | .unwrap_or_else(|err| { 187 | handle_error(&err, "Failed to verify ID token"); 188 | unreachable!(); 189 | }); 190 | println!("GitLab returned ID token: {:?}\n", id_token_claims); 191 | 192 | let userinfo_claims: UserInfoClaims = client 193 | .user_info(token_response.access_token().to_owned(), None) 194 | .unwrap_or_else(|err| { 195 | handle_error(&err, "No user info endpoint"); 196 | unreachable!(); 197 | }) 198 | .request(&http_client) 199 | .unwrap_or_else(|err| { 200 | handle_error(&err, "Failed requesting user info"); 201 | unreachable!(); 202 | }); 203 | println!("GitLab returned UserInfo: {:?}", userinfo_claims); 204 | } 205 | -------------------------------------------------------------------------------- /src/core/crypto.rs: -------------------------------------------------------------------------------- 1 | use crate::core::jwk::CoreJsonCurveType; 2 | use crate::core::{CoreJsonWebKey, CoreJsonWebKeyType}; 3 | use crate::helpers::Base64UrlEncodedBytes; 4 | use crate::{JsonWebKey, SignatureVerificationError}; 5 | 6 | use std::ops::Deref; 7 | 8 | fn rsa_public_key( 9 | key: &CoreJsonWebKey, 10 | ) -> Result<(&Base64UrlEncodedBytes, &Base64UrlEncodedBytes), String> { 11 | if *key.key_type() != CoreJsonWebKeyType::RSA { 12 | Err("RSA key required".to_string()) 13 | } else { 14 | let n = key 15 | .n 16 | .as_ref() 17 | .ok_or_else(|| "RSA modulus `n` is missing".to_string())?; 18 | let e = key 19 | .e 20 | .as_ref() 21 | .ok_or_else(|| "RSA exponent `e` is missing".to_string())?; 22 | Ok((n, e)) 23 | } 24 | } 25 | 26 | fn ec_public_key( 27 | key: &CoreJsonWebKey, 28 | ) -> Result< 29 | ( 30 | &Base64UrlEncodedBytes, 31 | &Base64UrlEncodedBytes, 32 | &CoreJsonCurveType, 33 | ), 34 | String, 35 | > { 36 | if *key.key_type() != CoreJsonWebKeyType::EllipticCurve { 37 | Err("EC key required".to_string()) 38 | } else { 39 | let x = key 40 | .x 41 | .as_ref() 42 | .ok_or_else(|| "EC `x` part is missing".to_string())?; 43 | let y = key 44 | .y 45 | .as_ref() 46 | .ok_or_else(|| "EC `y` part is missing".to_string())?; 47 | let crv = key 48 | .crv 49 | .as_ref() 50 | .ok_or_else(|| "EC `crv` part is missing".to_string())?; 51 | Ok((x, y, crv)) 52 | } 53 | } 54 | 55 | fn ed_public_key( 56 | key: &CoreJsonWebKey, 57 | ) -> Result<(&Base64UrlEncodedBytes, &CoreJsonCurveType), String> { 58 | if *key.key_type() != CoreJsonWebKeyType::OctetKeyPair { 59 | Err("OKP key required".to_string()) 60 | } else { 61 | let x = key 62 | .x 63 | .as_ref() 64 | .ok_or_else(|| "OKP `x` part is missing".to_string())?; 65 | let crv = key 66 | .crv 67 | .as_ref() 68 | .ok_or_else(|| "OKP `crv` part is missing".to_string())?; 69 | Ok((x, crv)) 70 | } 71 | } 72 | 73 | pub fn verify_rsa_signature( 74 | key: &CoreJsonWebKey, 75 | padding: impl rsa::traits::SignatureScheme, 76 | msg: &[u8], 77 | signature: &[u8], 78 | ) -> Result<(), SignatureVerificationError> { 79 | let (n, e) = rsa_public_key(key).map_err(SignatureVerificationError::InvalidKey)?; 80 | // let's n and e as a big integers to prevent issues with leading zeros 81 | // according to https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.1.1 82 | // `n` is always unsigned (hence has sign plus) 83 | 84 | let n_bigint = rsa::BigUint::from_bytes_be(n.deref()); 85 | let e_bigint = rsa::BigUint::from_bytes_be(e.deref()); 86 | let public_key = rsa::RsaPublicKey::new(n_bigint, e_bigint) 87 | .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; 88 | 89 | public_key 90 | .verify(padding, msg, signature) 91 | .map_err(|_| SignatureVerificationError::CryptoError("bad signature".to_string())) 92 | } 93 | /// According to RFC5480, Section-2.2 implementations of Elliptic Curve Cryptography MUST support the uncompressed form. 94 | /// The first octet of the octet string indicates whether the uncompressed or compressed form is used. For the uncompressed 95 | /// form, the first octet has to be 0x04. 96 | /// According to https://briansmith.org/rustdoc/ring/signature/index.html#ecdsa__fixed-details-fixed-length-pkcs11-style-ecdsa-signatures, 97 | /// to recover the X and Y coordinates from an octet string, the Octet-String-To-Elliptic-Curve-Point Conversion 98 | /// is used (Section 2.3.4 of https://www.secg.org/sec1-v2.pdf). 99 | 100 | pub fn verify_ec_signature( 101 | key: &CoreJsonWebKey, 102 | msg: &[u8], 103 | signature: &[u8], 104 | ) -> Result<(), SignatureVerificationError> { 105 | use p256::ecdsa::signature::Verifier; 106 | 107 | let (x, y, crv) = ec_public_key(key).map_err(SignatureVerificationError::InvalidKey)?; 108 | let mut pk = vec![0x04]; 109 | pk.extend(x.deref()); 110 | pk.extend(y.deref()); 111 | match *crv { 112 | CoreJsonCurveType::P256 => { 113 | let public_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&pk) 114 | .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; 115 | public_key 116 | .verify( 117 | msg, 118 | &p256::ecdsa::Signature::from_slice(signature).map_err(|_| { 119 | SignatureVerificationError::CryptoError("Invalid signature".to_string()) 120 | })?, 121 | ) 122 | .map_err(|_| { 123 | SignatureVerificationError::CryptoError("EC Signature was wrong".to_string()) 124 | }) 125 | } 126 | CoreJsonCurveType::P384 => { 127 | let public_key = p384::ecdsa::VerifyingKey::from_sec1_bytes(&pk) 128 | .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; 129 | public_key 130 | .verify( 131 | msg, 132 | &p384::ecdsa::Signature::from_slice(signature).map_err(|_| { 133 | SignatureVerificationError::CryptoError("Invalid signature".to_string()) 134 | })?, 135 | ) 136 | .map_err(|_| { 137 | SignatureVerificationError::CryptoError("EC Signature was wrong".to_string()) 138 | }) 139 | } 140 | CoreJsonCurveType::P521 => Err(SignatureVerificationError::UnsupportedAlg( 141 | "P521".to_string(), 142 | )), 143 | _ => Err(SignatureVerificationError::InvalidKey(format!( 144 | "unrecognized curve `{crv:?}`" 145 | ))), 146 | } 147 | } 148 | 149 | pub fn verify_ed_signature( 150 | key: &CoreJsonWebKey, 151 | msg: &[u8], 152 | signature: &[u8], 153 | ) -> Result<(), SignatureVerificationError> { 154 | use ed25519_dalek::Verifier; 155 | 156 | let (x, crv) = ed_public_key(key).map_err(SignatureVerificationError::InvalidKey)?; 157 | 158 | match *crv { 159 | CoreJsonCurveType::Ed25519 => { 160 | let public_key = ed25519_dalek::VerifyingKey::try_from(x.deref().as_slice()) 161 | .map_err(|e| SignatureVerificationError::InvalidKey(e.to_string()))?; 162 | 163 | public_key 164 | .verify( 165 | msg, 166 | &ed25519_dalek::Signature::from_slice(signature).map_err(|_| { 167 | SignatureVerificationError::CryptoError("invalid signature".to_string()) 168 | })?, 169 | ) 170 | .map_err(|_| { 171 | SignatureVerificationError::CryptoError("incorrect EdDSA signature".to_string()) 172 | }) 173 | } 174 | _ => Err(SignatureVerificationError::InvalidKey(format!( 175 | "unrecognized curve `{crv:?}`" 176 | ))), 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | use crate::core::crypto::verify_rsa_signature; 183 | use crate::core::CoreJsonWebKey; 184 | 185 | use base64::prelude::BASE64_URL_SAFE_NO_PAD; 186 | use base64::Engine; 187 | use sha2::Digest; 188 | 189 | #[test] 190 | fn test_leading_zeros_are_parsed_correctly() { 191 | // The message we signed 192 | let msg = "THIS IS A SIGNATURE TEST"; 193 | let signature = BASE64_URL_SAFE_NO_PAD.decode("bg0ohqKwYHAiODeG6qkJ-6IhodN7LGPxAh4hbWeIoBdSXrXMt8Ft8U0BV7vANPvF56h20XB9C0021x2kt7iAbMgPNcZ7LCuXMPPq04DrBpMHafH5BXBwnyDKJKrzDm5sfr6OgEkcxSLHaSJ6gTWQ3waPt6_SeH2-Fi74rg13MHyX-0iqz7bZveoBbGIs5yQCwvXgrDS9zW5LUwUHozHfE6FuSi_Z92ioXeu7FHHDg1KFfg3hs8ZLx4wAX15Vw2GCQOzvyNdbItxXRLnrN1NPqxFquVNo5RGlx6ihR1Jfe7y_n0NSR2q2TuU4cIwR0LRwEaANy5SDqtleQPrTEn8nGQ").unwrap(); 194 | // RSA pub key with leading 0 195 | let key : CoreJsonWebKey = serde_json::from_value(serde_json::json!( 196 | { 197 | "kty": "RSA", 198 | "e": "AQAB", 199 | "use": "sig", 200 | "kid": "TEST_KEY_ID", 201 | "alg": "RS256", 202 | "n": "AN0M6Y760b9Ok2PxDOps1TgSmiOaR9mLIfUHtZ_o-6JypOckGcl1CxrteyokOb3WyDsfIAN9fFNrycv5YoLKO7sh0IcfzNEXFgzK84HTBcGuqhN8NV98Z6N9EryUrgJYsJeVoPYm0MzkDe4NyWHhnq-9OyNCQzVELH0NhhViQqRyM92OPrJcQlk8s3ZvcgRmkd-rEtRua8SbS3GEvfvgweVy5-qcJCGoziKfx-IteMOm6yKoHvqisKb91N-qw_kSS4YQUx-DZVDo2g24F7VIbcYzJGUOU674HUF1j-wJyXzG3VV8lAXD8hABs5Lh87gr8_hIZD5gbYBJRObJk9XZbfk" 203 | } 204 | )).unwrap(); 205 | 206 | let mut hasher = sha2::Sha256::new(); 207 | hasher.update(msg); 208 | let hash = hasher.finalize().to_vec(); 209 | assert! { 210 | verify_rsa_signature( 211 | &key, 212 | rsa::Pkcs1v15Sign::new::(), 213 | &hash, 214 | &signature, 215 | ).is_ok() 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /examples/google.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! This example showcases the process of integrating with the 3 | //! [Google OpenID Connect](https://developers.google.com/identity/protocols/OpenIDConnect) 4 | //! provider. 5 | //! 6 | //! Before running it, you'll need to generate your own Google OAuth2 credentials. 7 | //! 8 | //! In order to run the example call: 9 | //! 10 | //! ```sh 11 | //! GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy cargo run --example google 12 | //! ``` 13 | //! 14 | //! ...and follow the instructions. 15 | //! 16 | 17 | use openidconnect::core::{ 18 | CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClient, CoreClientAuthMethod, CoreGrantType, 19 | CoreIdTokenClaims, CoreIdTokenVerifier, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, 20 | CoreJweKeyManagementAlgorithm, CoreResponseMode, CoreResponseType, CoreRevocableToken, 21 | CoreSubjectIdentifierType, 22 | }; 23 | use openidconnect::reqwest; 24 | use openidconnect::{ 25 | AdditionalProviderMetadata, AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, 26 | CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse, ProviderMetadata, RedirectUrl, RevocationUrl, 27 | Scope, 28 | }; 29 | use serde::{Deserialize, Serialize}; 30 | use url::Url; 31 | 32 | use std::env; 33 | use std::io::{BufRead, BufReader, Write}; 34 | use std::net::TcpListener; 35 | use std::process::exit; 36 | 37 | fn handle_error(fail: &T, msg: &'static str) { 38 | let mut err_msg = format!("ERROR: {}", msg); 39 | let mut cur_fail: Option<&dyn std::error::Error> = Some(fail); 40 | while let Some(cause) = cur_fail { 41 | err_msg += &format!("\n caused by: {}", cause); 42 | cur_fail = cause.source(); 43 | } 44 | println!("{}", err_msg); 45 | exit(1); 46 | } 47 | 48 | // Teach openidconnect-rs about a Google custom extension to the OpenID Discovery response that we can use as the RFC 49 | // 7009 OAuth 2.0 Token Revocation endpoint. For more information about the Google specific Discovery response see the 50 | // Google OpenID Connect service documentation at: https://developers.google.com/identity/protocols/oauth2/openid-connect#discovery 51 | #[derive(Clone, Debug, Deserialize, Serialize)] 52 | struct RevocationEndpointProviderMetadata { 53 | revocation_endpoint: String, 54 | } 55 | impl AdditionalProviderMetadata for RevocationEndpointProviderMetadata {} 56 | type GoogleProviderMetadata = ProviderMetadata< 57 | RevocationEndpointProviderMetadata, 58 | CoreAuthDisplay, 59 | CoreClientAuthMethod, 60 | CoreClaimName, 61 | CoreClaimType, 62 | CoreGrantType, 63 | CoreJweContentEncryptionAlgorithm, 64 | CoreJweKeyManagementAlgorithm, 65 | CoreJsonWebKey, 66 | CoreResponseMode, 67 | CoreResponseType, 68 | CoreSubjectIdentifierType, 69 | >; 70 | 71 | fn main() { 72 | env_logger::init(); 73 | 74 | let google_client_id = ClientId::new( 75 | env::var("GOOGLE_CLIENT_ID").expect("Missing the GOOGLE_CLIENT_ID environment variable."), 76 | ); 77 | let google_client_secret = ClientSecret::new( 78 | env::var("GOOGLE_CLIENT_SECRET") 79 | .expect("Missing the GOOGLE_CLIENT_SECRET environment variable."), 80 | ); 81 | let issuer_url = 82 | IssuerUrl::new("https://accounts.google.com".to_string()).unwrap_or_else(|err| { 83 | handle_error(&err, "Invalid issuer URL"); 84 | unreachable!(); 85 | }); 86 | 87 | let http_client = reqwest::blocking::ClientBuilder::new() 88 | // Following redirects opens the client up to SSRF vulnerabilities. 89 | .redirect(reqwest::redirect::Policy::none()) 90 | .build() 91 | .unwrap_or_else(|err| { 92 | handle_error(&err, "Failed to build HTTP client"); 93 | unreachable!(); 94 | }); 95 | 96 | // Fetch Google's OpenID Connect discovery document. 97 | // 98 | // Note: If we don't care about token revocation we can simply use CoreProviderMetadata here 99 | // instead of GoogleProviderMetadata. If instead we wanted to optionally use the token 100 | // revocation endpoint if it seems to be supported we could do something like this: 101 | // #[derive(Clone, Debug, Deserialize, Serialize)] 102 | // struct AllOtherProviderMetadata(HashMap); 103 | // impl AdditionalClaims for AllOtherProviderMetadata {} 104 | // And then test for the presence of "revocation_endpoint" in the map returned by a call to 105 | // .additional_metadata(). 106 | 107 | let provider_metadata = GoogleProviderMetadata::discover(&issuer_url, &http_client) 108 | .unwrap_or_else(|err| { 109 | handle_error(&err, "Failed to discover OpenID Provider"); 110 | unreachable!(); 111 | }); 112 | 113 | let revocation_endpoint = provider_metadata 114 | .additional_metadata() 115 | .revocation_endpoint 116 | .clone(); 117 | println!( 118 | "Discovered Google revocation endpoint: {}", 119 | revocation_endpoint 120 | ); 121 | 122 | // Set up the config for the Google OAuth2 process. 123 | let client = CoreClient::from_provider_metadata( 124 | provider_metadata, 125 | google_client_id, 126 | Some(google_client_secret), 127 | ) 128 | // This example will be running its own server at localhost:8080. 129 | // See below for the server implementation. 130 | .set_redirect_uri( 131 | RedirectUrl::new("http://localhost:8080".to_string()).unwrap_or_else(|err| { 132 | handle_error(&err, "Invalid redirect URL"); 133 | unreachable!(); 134 | }), 135 | ) 136 | // Google supports OAuth 2.0 Token Revocation (RFC-7009) 137 | .set_revocation_url( 138 | RevocationUrl::new(revocation_endpoint).unwrap_or_else(|err| { 139 | handle_error(&err, "Invalid revocation endpoint URL"); 140 | unreachable!(); 141 | }), 142 | ); 143 | 144 | // Generate the authorization URL to which we'll redirect the user. 145 | let (authorize_url, csrf_state, nonce) = client 146 | .authorize_url( 147 | AuthenticationFlow::::AuthorizationCode, 148 | CsrfToken::new_random, 149 | Nonce::new_random, 150 | ) 151 | // This example is requesting access to the "calendar" features and the user's profile. 152 | .add_scope(Scope::new("email".to_string())) 153 | .add_scope(Scope::new("profile".to_string())) 154 | .url(); 155 | 156 | println!("Open this URL in your browser:\n{}\n", authorize_url); 157 | 158 | let (code, state) = { 159 | // A very naive implementation of the redirect server. 160 | let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); 161 | 162 | // Accept one connection 163 | let (mut stream, _) = listener.accept().unwrap(); 164 | 165 | let mut reader = BufReader::new(&stream); 166 | 167 | let mut request_line = String::new(); 168 | reader.read_line(&mut request_line).unwrap(); 169 | 170 | let redirect_url = request_line.split_whitespace().nth(1).unwrap(); 171 | let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); 172 | 173 | let code = url 174 | .query_pairs() 175 | .find(|(key, _)| key == "code") 176 | .map(|(_, code)| AuthorizationCode::new(code.into_owned())) 177 | .unwrap(); 178 | 179 | let state = url 180 | .query_pairs() 181 | .find(|(key, _)| key == "state") 182 | .map(|(_, state)| CsrfToken::new(state.into_owned())) 183 | .unwrap(); 184 | 185 | let message = "Go back to your terminal :)"; 186 | let response = format!( 187 | "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", 188 | message.len(), 189 | message 190 | ); 191 | stream.write_all(response.as_bytes()).unwrap(); 192 | 193 | (code, state) 194 | }; 195 | 196 | println!("Google returned the following code:\n{}\n", code.secret()); 197 | println!( 198 | "Google returned the following state:\n{} (expected `{}`)\n", 199 | state.secret(), 200 | csrf_state.secret() 201 | ); 202 | 203 | // Exchange the code with a token. 204 | let token_response = client 205 | .exchange_code(code) 206 | .unwrap_or_else(|err| { 207 | handle_error(&err, "No user info endpoint"); 208 | unreachable!(); 209 | }) 210 | .request(&http_client) 211 | .unwrap_or_else(|err| { 212 | handle_error(&err, "Failed to contact token endpoint"); 213 | unreachable!(); 214 | }); 215 | 216 | println!( 217 | "Google returned access token:\n{}\n", 218 | token_response.access_token().secret() 219 | ); 220 | println!("Google returned scopes: {:?}", token_response.scopes()); 221 | 222 | let id_token_verifier: CoreIdTokenVerifier = client.id_token_verifier(); 223 | let id_token_claims: &CoreIdTokenClaims = token_response 224 | .extra_fields() 225 | .id_token() 226 | .expect("Server did not return an ID token") 227 | .claims(&id_token_verifier, &nonce) 228 | .unwrap_or_else(|err| { 229 | handle_error(&err, "Failed to verify ID token"); 230 | unreachable!(); 231 | }); 232 | println!("Google returned ID token: {:?}", id_token_claims); 233 | 234 | // Revoke the obtained token 235 | let token_to_revoke: CoreRevocableToken = match token_response.refresh_token() { 236 | Some(token) => token.into(), 237 | None => token_response.access_token().into(), 238 | }; 239 | 240 | client 241 | .revoke_token(token_to_revoke) 242 | .unwrap_or_else(|err| { 243 | handle_error(&err, "Failed to revoke token"); 244 | unreachable!(); 245 | }) 246 | .request(&http_client) 247 | .unwrap_or_else(|err| { 248 | handle_error(&err, "Failed to revoke token"); 249 | unreachable!(); 250 | }); 251 | } 252 | -------------------------------------------------------------------------------- /src/logout.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{ 2 | CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, 3 | CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, 4 | CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, 5 | }; 6 | use crate::helpers::join_vec; 7 | use crate::types::{LogoutHint, PostLogoutRedirectUrl}; 8 | use crate::{ 9 | AdditionalClaims, AdditionalProviderMetadata, ClientId, CsrfToken, 10 | EmptyAdditionalProviderMetadata, EndSessionUrl, GenderClaim, IdToken, 11 | JweContentEncryptionAlgorithm, JwsSigningAlgorithm, LanguageTag, ProviderMetadata, 12 | }; 13 | 14 | use serde::{Deserialize, Serialize}; 15 | use serde_with::skip_serializing_none; 16 | use url::Url; 17 | 18 | /// Additional metadata for providers implementing [OpenID Connect RP-Initiated 19 | /// Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html). 20 | #[skip_serializing_none] 21 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] 22 | pub struct LogoutProviderMetadata 23 | where 24 | A: AdditionalProviderMetadata, 25 | { 26 | /// The end session endpoint as described in [OpenID Connect RP-Initiated 27 | /// Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html). 28 | pub end_session_endpoint: Option, 29 | #[serde(bound = "A: AdditionalProviderMetadata", flatten)] 30 | /// A field for an additional struct implementing AdditionalProviderMetadata. 31 | pub additional_metadata: A, 32 | } 33 | impl AdditionalProviderMetadata for LogoutProviderMetadata where A: AdditionalProviderMetadata {} 34 | 35 | /// Provider metadata returned by [OpenID Connect Discovery]( 36 | /// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata) 37 | /// that returns [`ProviderMetadata::additional_metadata`] for providers 38 | /// implementing [OpenID Connect RP-Initiated Logout 1.0]( 39 | /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html). 40 | pub type ProviderMetadataWithLogout = ProviderMetadata< 41 | LogoutProviderMetadata, 42 | CoreAuthDisplay, 43 | CoreClientAuthMethod, 44 | CoreClaimName, 45 | CoreClaimType, 46 | CoreGrantType, 47 | CoreJweContentEncryptionAlgorithm, 48 | CoreJweKeyManagementAlgorithm, 49 | CoreJsonWebKey, 50 | CoreResponseMode, 51 | CoreResponseType, 52 | CoreSubjectIdentifierType, 53 | >; 54 | 55 | /// A request to the end session endpoint. 56 | pub struct LogoutRequest { 57 | end_session_endpoint: EndSessionUrl, 58 | parameters: LogoutRequestParameters, 59 | } 60 | 61 | #[derive(Default)] 62 | struct LogoutRequestParameters { 63 | id_token_hint: Option, 64 | logout_hint: Option, 65 | client_id: Option, 66 | post_logout_redirect_uri: Option, 67 | state: Option, 68 | ui_locales: Vec, 69 | } 70 | 71 | impl From for LogoutRequest { 72 | fn from(value: EndSessionUrl) -> Self { 73 | LogoutRequest { 74 | end_session_endpoint: value, 75 | parameters: Default::default(), 76 | } 77 | } 78 | } 79 | 80 | impl LogoutRequest { 81 | /// Provides an ID token previously issued by this OpenID Connect Provider as a hint about 82 | /// the user's identity. 83 | pub fn set_id_token_hint( 84 | mut self, 85 | id_token_hint: &IdToken, 86 | ) -> Self 87 | where 88 | AC: AdditionalClaims, 89 | GC: GenderClaim, 90 | JE: JweContentEncryptionAlgorithm, 91 | JS: JwsSigningAlgorithm, 92 | { 93 | self.parameters.id_token_hint = Some(id_token_hint.to_string()); 94 | self 95 | } 96 | 97 | /// Provides the OpenID Connect Provider with a hint about the user's identity. 98 | /// 99 | /// The nature of this hint is specific to each provider. 100 | pub fn set_logout_hint(mut self, logout_hint: LogoutHint) -> Self { 101 | self.parameters.logout_hint = Some(logout_hint); 102 | self 103 | } 104 | 105 | /// Provides the OpenID Connect Provider with the client identifier. 106 | /// 107 | /// When both this and `id_token_hint` are set, the provider must verify that 108 | /// this client id matches the one used when the ID token was issued. 109 | pub fn set_client_id(mut self, client_id: ClientId) -> Self { 110 | self.parameters.client_id = Some(client_id); 111 | self 112 | } 113 | 114 | /// Provides the OpenID Connect Provider with a URI to redirect to after 115 | /// the logout has been performed. 116 | pub fn set_post_logout_redirect_uri(mut self, redirect_uri: PostLogoutRedirectUrl) -> Self { 117 | self.parameters.post_logout_redirect_uri = Some(redirect_uri); 118 | self 119 | } 120 | 121 | /// Specify an opaque value that the OpenID Connect Provider should pass back 122 | /// to your application using the state parameter when redirecting to post_logout_redirect_uri. 123 | pub fn set_state(mut self, state: CsrfToken) -> Self { 124 | self.parameters.state = Some(state); 125 | self 126 | } 127 | 128 | /// Requests the preferred languages for the user interface presented by the OpenID Connect 129 | /// Provider. 130 | /// 131 | /// Languages should be added in order of preference. 132 | pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self { 133 | self.parameters.ui_locales.push(ui_locale); 134 | self 135 | } 136 | 137 | /// Returns the full logout URL. In order to logout, a GET request should be made to this URL 138 | /// by the client's browser. 139 | pub fn http_get_url(self) -> Url { 140 | let mut url = self.end_session_endpoint.url().to_owned(); 141 | { 142 | let mut query = url.query_pairs_mut(); 143 | 144 | macro_rules! add_pair { 145 | ($name:ident, $acc:expr) => { 146 | if let Some($name) = self.parameters.$name { 147 | query.append_pair(stringify!($name), $acc); 148 | } 149 | }; 150 | } 151 | 152 | add_pair!(id_token_hint, id_token_hint.as_str()); 153 | add_pair!(logout_hint, logout_hint.secret()); 154 | add_pair!(client_id, client_id.as_str()); 155 | add_pair!(post_logout_redirect_uri, post_logout_redirect_uri.as_str()); 156 | add_pair!(state, state.secret()); 157 | 158 | if !self.parameters.ui_locales.is_empty() { 159 | query.append_pair("ui_locales", &join_vec(&self.parameters.ui_locales)); 160 | } 161 | } 162 | 163 | if url.query() == Some("") { 164 | url.set_query(None); 165 | } 166 | 167 | url 168 | } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use crate::core::{ 174 | CoreGenderClaim, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, 175 | }; 176 | use crate::types::{LogoutHint, PostLogoutRedirectUrl}; 177 | use crate::{ 178 | AuthUrl, ClientId, CsrfToken, EmptyAdditionalClaims, EndSessionUrl, IdToken, IssuerUrl, 179 | JsonWebKeySetUrl, LanguageTag, LogoutProviderMetadata, LogoutRequest, 180 | ProviderMetadataWithLogout, 181 | }; 182 | 183 | use url::Url; 184 | 185 | use std::str::FromStr; 186 | 187 | #[test] 188 | fn test_end_session_endpoint_deserialization() { 189 | // Fetched from: https://rp.certification.openid.net:8080/openidconnect-rs/ 190 | // rp-response_type-code/.well-known/openid-configuration 191 | // But pared down 192 | let json_response = "{\ 193 | \"issuer\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code\",\ 194 | \"authorization_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/authorization\",\ 195 | \"jwks_uri\":\"https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json\",\ 196 | \"response_types_supported\":[],\ 197 | \"subject_types_supported\":[],\ 198 | \"id_token_signing_alg_values_supported\": [],\ 199 | \"end_session_endpoint\":\"https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session\",\ 200 | \"version\":\"3.0\"}"; 201 | 202 | let new_provider_metadata = ProviderMetadataWithLogout::new( 203 | IssuerUrl::new( 204 | "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code" 205 | .to_string(), 206 | ) 207 | .unwrap(), 208 | AuthUrl::new( 209 | "https://rp.certification.openid.net:8080/openidconnect-rs/\ 210 | rp-response_type-code/authorization" 211 | .to_string(), 212 | ) 213 | .unwrap(), 214 | JsonWebKeySetUrl::new( 215 | "https://rp.certification.openid.net:8080/static/jwks_3INbZl52IrrPCp2j.json" 216 | .to_string(), 217 | ) 218 | .unwrap(), 219 | vec![], 220 | vec![], 221 | vec![], 222 | LogoutProviderMetadata { 223 | end_session_endpoint: Some(EndSessionUrl::new( 224 | "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" 225 | .to_string() 226 | ).unwrap()), 227 | additional_metadata: Default::default(), 228 | }, 229 | ); 230 | 231 | let provider_metadata: ProviderMetadataWithLogout = 232 | serde_json::from_str(json_response).unwrap(); 233 | assert_eq!(provider_metadata, new_provider_metadata); 234 | 235 | assert_eq!( 236 | Some(EndSessionUrl::new( 237 | "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" 238 | .to_string() 239 | ).unwrap()), 240 | provider_metadata.additional_metadata().end_session_endpoint 241 | ); 242 | } 243 | 244 | #[test] 245 | fn test_logout_request_with_no_parameters() { 246 | let endpoint = EndSessionUrl::new( 247 | "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" 248 | .to_string() 249 | ).unwrap(); 250 | 251 | let logout_url = LogoutRequest::from(endpoint).http_get_url(); 252 | 253 | assert_eq!( 254 | Url::parse( 255 | "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" 256 | ).unwrap(), 257 | logout_url 258 | ); 259 | } 260 | 261 | #[test] 262 | fn test_logout_request_with_all_parameters() { 263 | let endpoint = EndSessionUrl::new( 264 | "https://rp.certification.openid.net:8080/openidconnect-rs/rp-response_type-code/end_session" 265 | .to_string() 266 | ).unwrap(); 267 | 268 | let logout_url = LogoutRequest::from(endpoint) 269 | .set_id_token_hint( 270 | &IdToken::< 271 | EmptyAdditionalClaims, 272 | CoreGenderClaim, 273 | CoreJweContentEncryptionAlgorithm, 274 | CoreJwsSigningAlgorithm, 275 | >::from_str( 276 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwcz\ 277 | ovL3JwLmNlcnRpZmljYXRpb24ub3BlbmlkLm5ldDo4MDgwLyIsImV4c\ 278 | CI6MTUxNjIzOTAyMiwiaWF0IjoxNTE2MjM5MDIyLCJzdWIiOiJhc2Rm\ 279 | In0.cPwX6csO2uBEOZLVAGR7x5rHLRfD36MHpPy3JTk6orM", 280 | ) 281 | .unwrap(), 282 | ) 283 | .set_logout_hint(LogoutHint::new("johndoe".to_string())) 284 | .set_client_id(ClientId::new("asdf".to_string())) 285 | .set_post_logout_redirect_uri( 286 | PostLogoutRedirectUrl::new("https://localhost:8000/".to_string()).unwrap(), 287 | ) 288 | .set_state(CsrfToken::new("asdf".to_string())) 289 | .add_ui_locale(LanguageTag::new("en-US".to_string())) 290 | .add_ui_locale(LanguageTag::new("fr-FR".to_string())) 291 | .http_get_url(); 292 | 293 | assert_eq!( 294 | Url::parse( 295 | "https://rp.certification.openid.net:8080/openidconnect-rs\ 296 | /rp-response_type-code/end_session?id_token_hint=eyJhbGciO\ 297 | iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JwLmNlcn\ 298 | RpZmljYXRpb24ub3BlbmlkLm5ldDo4MDgwLyIsImV4cCI6MTUxNjIzOTAy\ 299 | MiwiaWF0IjoxNTE2MjM5MDIyLCJzdWIiOiJhc2RmIn0.cPwX6csO2uBEOZ\ 300 | LVAGR7x5rHLRfD36MHpPy3JTk6orM&logout_hint=johndoe&client_i\ 301 | d=asdf&post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A\ 302 | 8000%2F&state=asdf&ui_locales=en-US+fr-FR" 303 | ) 304 | .unwrap(), 305 | logout_url 306 | ); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::types::jwk::JsonWebKey; 2 | use crate::{AccessToken, AuthorizationCode}; 3 | 4 | use base64::prelude::BASE64_URL_SAFE_NO_PAD; 5 | use base64::Engine; 6 | use oauth2::helpers::deserialize_space_delimited_vec; 7 | use rand::{thread_rng, Rng}; 8 | use serde::de::DeserializeOwned; 9 | use serde::{Deserialize, Serialize}; 10 | use thiserror::Error; 11 | use url::Url; 12 | 13 | use std::fmt::Debug; 14 | use std::hash::Hash; 15 | use std::ops::Deref; 16 | 17 | pub(crate) mod jwk; 18 | pub(crate) mod jwks; 19 | pub(crate) mod localized; 20 | 21 | #[cfg(test)] 22 | mod tests; 23 | 24 | /// Client application type. 25 | pub trait ApplicationType: Debug + DeserializeOwned + Serialize + 'static {} 26 | 27 | /// How the Authorization Server displays the authentication and consent user interface pages to 28 | /// the End-User. 29 | pub trait AuthDisplay: AsRef + Debug + DeserializeOwned + Serialize + 'static {} 30 | 31 | /// Whether the Authorization Server should prompt the End-User for reauthentication and consent. 32 | pub trait AuthPrompt: AsRef + 'static {} 33 | 34 | /// Claim name. 35 | pub trait ClaimName: Debug + DeserializeOwned + Serialize + 'static {} 36 | 37 | /// Claim type (e.g., normal, aggregated, or distributed). 38 | pub trait ClaimType: Debug + DeserializeOwned + Serialize + 'static {} 39 | 40 | /// Client authentication method. 41 | pub trait ClientAuthMethod: Debug + DeserializeOwned + Serialize + 'static {} 42 | 43 | /// Grant type. 44 | pub trait GrantType: Debug + DeserializeOwned + Serialize + 'static {} 45 | 46 | /// Error signing a message. 47 | #[derive(Clone, Debug, Error, PartialEq, Eq)] 48 | #[non_exhaustive] 49 | pub enum SigningError { 50 | /// Failed to sign the message using the given key and parameters. 51 | #[error("Crypto error")] 52 | CryptoError, 53 | /// Unsupported signature algorithm. 54 | #[error("Unsupported signature algorithm: {0}")] 55 | UnsupportedAlg(String), 56 | /// An unexpected error occurred. 57 | #[error("Other error: {0}")] 58 | Other(String), 59 | } 60 | 61 | /// Response mode indicating how the OpenID Connect Provider should return the Authorization 62 | /// Response to the Relying Party (client). 63 | pub trait ResponseMode: Debug + DeserializeOwned + Serialize + 'static {} 64 | 65 | /// Response type indicating the desired authorization processing flow, including what 66 | /// parameters are returned from the endpoints used. 67 | pub trait ResponseType: AsRef + Debug + DeserializeOwned + Serialize + 'static { 68 | /// Converts this OpenID Connect response type to an [`oauth2::ResponseType`] used by the 69 | /// underlying [`oauth2`] crate. 70 | fn to_oauth2(&self) -> oauth2::ResponseType; 71 | } 72 | 73 | /// Subject identifier type returned by an OpenID Connect Provider to uniquely identify its users. 74 | pub trait SubjectIdentifierType: Debug + DeserializeOwned + Serialize + 'static {} 75 | 76 | new_type![ 77 | /// Set of authentication methods or procedures that are considered to be equivalent to each 78 | /// other in a particular context. 79 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 80 | AuthenticationContextClass(String) 81 | ]; 82 | impl AsRef for AuthenticationContextClass { 83 | fn as_ref(&self) -> &str { 84 | self 85 | } 86 | } 87 | 88 | new_type![ 89 | /// Identifier for an authentication method (e.g., `password` or `totp`). 90 | /// 91 | /// Defining specific AMR identifiers is beyond the scope of the OpenID Connect Core spec. 92 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 93 | AuthenticationMethodReference(String) 94 | ]; 95 | 96 | new_type![ 97 | /// Access token hash. 98 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 99 | AccessTokenHash(String) 100 | impl { 101 | /// Initialize a new access token hash from an [`AccessToken`] and signature algorithm. 102 | pub fn from_token( 103 | access_token: &AccessToken, 104 | alg: &K::SigningAlgorithm, 105 | key: &K, 106 | ) -> Result 107 | where 108 | K: JsonWebKey, 109 | { 110 | key.hash_bytes(access_token.secret().as_bytes(), alg) 111 | .map(|hash| Self::new(BASE64_URL_SAFE_NO_PAD.encode(&hash[0..hash.len() / 2]))) 112 | .map_err(SigningError::UnsupportedAlg) 113 | } 114 | } 115 | ]; 116 | 117 | new_type![ 118 | /// Country portion of address. 119 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 120 | AddressCountry(String) 121 | ]; 122 | 123 | new_type![ 124 | /// Locality portion of address. 125 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 126 | AddressLocality(String) 127 | ]; 128 | 129 | new_type![ 130 | /// Postal code portion of address. 131 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 132 | AddressPostalCode(String) 133 | ]; 134 | 135 | new_type![ 136 | /// Region portion of address. 137 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 138 | AddressRegion(String) 139 | ]; 140 | 141 | new_type![ 142 | /// Audience claim value. 143 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 144 | Audience(String) 145 | ]; 146 | 147 | new_type![ 148 | /// Authorization code hash. 149 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 150 | AuthorizationCodeHash(String) 151 | impl { 152 | /// Initialize a new authorization code hash from an [`AuthorizationCode`] and signature 153 | /// algorithm. 154 | pub fn from_code( 155 | code: &AuthorizationCode, 156 | alg: &K::SigningAlgorithm, 157 | key: &K, 158 | ) -> Result 159 | where 160 | K: JsonWebKey, 161 | { 162 | key.hash_bytes(code.secret().as_bytes(), alg) 163 | .map(|hash| Self::new(BASE64_URL_SAFE_NO_PAD.encode(&hash[0..hash.len() / 2]))) 164 | .map_err(SigningError::UnsupportedAlg) 165 | } 166 | } 167 | ]; 168 | 169 | new_type![ 170 | /// OpenID Connect client name. 171 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 172 | ClientName(String) 173 | ]; 174 | 175 | new_url_type![ 176 | /// Client configuration endpoint URL. 177 | ClientConfigUrl 178 | ]; 179 | 180 | new_url_type![ 181 | /// Client homepage URL. 182 | ClientUrl 183 | ]; 184 | 185 | new_type![ 186 | /// Client contact e-mail address. 187 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 188 | ClientContactEmail(String) 189 | ]; 190 | 191 | new_url_type![ 192 | /// URL for the [OpenID Connect RP-Initiated Logout 1.0]( 193 | /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html) end session endpoint. 194 | EndSessionUrl 195 | ]; 196 | 197 | new_type![ 198 | /// End user's birthday, represented as an 199 | /// [ISO 8601:2004](https://www.iso.org/standard/40874.html) `YYYY-MM-DD` format. 200 | /// 201 | /// The year MAY be `0000`, indicating that it is omitted. To represent only the year, `YYYY` 202 | /// format is allowed. Note that depending on the underlying platform's date related function, 203 | /// providing just year can result in varying month and day, so the implementers need to take 204 | /// this factor into account to correctly process the dates. 205 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 206 | EndUserBirthday(String) 207 | ]; 208 | 209 | new_type![ 210 | /// End user's e-mail address. 211 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 212 | EndUserEmail(String) 213 | ]; 214 | 215 | new_type![ 216 | /// End user's family name. 217 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 218 | EndUserFamilyName(String) 219 | ]; 220 | 221 | new_type![ 222 | /// End user's given name. 223 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 224 | EndUserGivenName(String) 225 | ]; 226 | 227 | new_type![ 228 | /// End user's middle name. 229 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 230 | EndUserMiddleName(String) 231 | ]; 232 | 233 | new_type![ 234 | /// End user's name. 235 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 236 | EndUserName(String) 237 | ]; 238 | 239 | new_type![ 240 | /// End user's nickname. 241 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 242 | EndUserNickname(String) 243 | ]; 244 | 245 | new_type![ 246 | /// End user's phone number. 247 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 248 | EndUserPhoneNumber(String) 249 | ]; 250 | 251 | new_type![ 252 | /// URL of end user's profile picture. 253 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 254 | EndUserPictureUrl(String) 255 | ]; 256 | 257 | new_type![ 258 | /// URL of end user's profile page. 259 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 260 | EndUserProfileUrl(String) 261 | ]; 262 | 263 | new_type![ 264 | /// End user's time zone as a string from the 265 | /// [time zone database](https://www.iana.org/time-zones). 266 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 267 | EndUserTimezone(String) 268 | ]; 269 | 270 | new_type![ 271 | /// URL of end user's website. 272 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 273 | EndUserWebsiteUrl(String) 274 | ]; 275 | 276 | new_type![ 277 | /// End user's username. 278 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 279 | EndUserUsername(String) 280 | ]; 281 | 282 | new_type![ 283 | /// Full mailing address, formatted for display or use on a mailing label. 284 | /// 285 | /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented 286 | /// either as a carriage return/line feed pair (`"\r\n"`) or as a single line feed character 287 | /// (`"\n"`). 288 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 289 | FormattedAddress(String) 290 | ]; 291 | 292 | new_url_type![ 293 | /// URI using the `https` scheme that a third party can use to initiate a login by the Relying 294 | /// Party. 295 | InitiateLoginUrl 296 | ]; 297 | 298 | new_url_type![ 299 | /// URL using the `https` scheme with no query or fragment component that the OP asserts as its 300 | /// Issuer Identifier. 301 | IssuerUrl 302 | impl { 303 | /// Parse a string as a URL, with this URL as the base URL. 304 | /// 305 | /// See [`Url::parse`]. 306 | pub fn join(&self, suffix: &str) -> Result { 307 | if let Some('/') = self.1.chars().next_back() { 308 | Url::parse(&(self.1.clone() + suffix)) 309 | } else { 310 | Url::parse(&(self.1.clone() + "/" + suffix)) 311 | } 312 | } 313 | } 314 | ]; 315 | 316 | new_secret_type![ 317 | /// Hint about the login identifier the End-User might use to log in. 318 | /// 319 | /// The use of this parameter is left to the OpenID Connect Provider's discretion. 320 | #[derive(Clone, Deserialize, Serialize)] 321 | LoginHint(String) 322 | ]; 323 | 324 | new_secret_type![ 325 | /// Hint about the logout identifier the End-User might use to log out. 326 | /// 327 | /// The use of this parameter is left to the OpenID Connect Provider's discretion. 328 | #[derive(Clone, Deserialize, Serialize)] 329 | LogoutHint(String) 330 | ]; 331 | 332 | new_url_type![ 333 | /// URL that references a logo for the Client application. 334 | LogoUrl 335 | ]; 336 | 337 | new_secret_type![ 338 | /// String value used to associate a client session with an ID Token, and to mitigate replay 339 | /// attacks. 340 | #[derive(Clone, Deserialize, Serialize)] 341 | Nonce(String) 342 | impl { 343 | /// Generate a new random, base64-encoded 128-bit nonce. 344 | pub fn new_random() -> Self { 345 | Nonce::new_random_len(16) 346 | } 347 | /// Generate a new random, base64-encoded nonce of the specified length. 348 | /// 349 | /// # Arguments 350 | /// 351 | /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. 352 | pub fn new_random_len(num_bytes: u32) -> Self { 353 | let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); 354 | Nonce::new(BASE64_URL_SAFE_NO_PAD.encode(random_bytes)) 355 | } 356 | } 357 | ]; 358 | 359 | new_url_type![ 360 | /// URL providing the OpenID Connect Provider's data usage policies for client applications. 361 | OpPolicyUrl 362 | ]; 363 | 364 | new_url_type![ 365 | /// URL providing the OpenID Connect Provider's Terms of Service. 366 | OpTosUrl 367 | ]; 368 | 369 | new_url_type![ 370 | /// URL providing a client application's data usage policy. 371 | PolicyUrl 372 | ]; 373 | 374 | new_url_type![ 375 | /// The post logout redirect URL, which should be passed to the end session endpoint 376 | /// of providers implementing [OpenID Connect RP-Initiated Logout 1.0]( 377 | /// https://openid.net/specs/openid-connect-rpinitiated-1_0.html). 378 | PostLogoutRedirectUrl 379 | ]; 380 | 381 | new_secret_type![ 382 | /// Access token used by a client application to access the Client Registration endpoint. 383 | #[derive(Clone, Deserialize, Serialize)] 384 | RegistrationAccessToken(String) 385 | ]; 386 | 387 | new_url_type![ 388 | /// URL of the Client Registration endpoint. 389 | RegistrationUrl 390 | ]; 391 | 392 | new_url_type![ 393 | /// URL used to pass request parameters as JWTs by reference. 394 | RequestUrl 395 | ]; 396 | 397 | /// Informs the Authorization Server of the desired authorization processing flow, including what 398 | /// parameters are returned from the endpoints used. 399 | /// 400 | /// See [OAuth 2.0 Multiple Response Type Encoding Practices]( 401 | /// http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseTypesAndModes) 402 | /// for further details. 403 | #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] 404 | pub struct ResponseTypes( 405 | #[serde( 406 | deserialize_with = "deserialize_space_delimited_vec", 407 | serialize_with = "crate::helpers::serialize_space_delimited_vec" 408 | )] 409 | Vec, 410 | ); 411 | impl ResponseTypes { 412 | /// Create a new [`ResponseTypes`] to wrap the given [`Vec`]. 413 | pub fn new(s: Vec) -> Self { 414 | ResponseTypes::(s) 415 | } 416 | } 417 | impl Deref for ResponseTypes { 418 | type Target = Vec; 419 | fn deref(&self) -> &Vec { 420 | &self.0 421 | } 422 | } 423 | 424 | new_url_type![ 425 | /// URL for retrieving redirect URIs that should receive identical pairwise subject identifiers. 426 | SectorIdentifierUrl 427 | ]; 428 | 429 | new_url_type![ 430 | /// URL for developer documentation for an OpenID Connect Provider. 431 | ServiceDocUrl 432 | ]; 433 | 434 | new_type![ 435 | /// A user's street address. 436 | /// 437 | /// Full street address component, which MAY include house number, street name, Post Office Box, 438 | /// and multi-line extended street address information. This field MAY contain multiple lines, 439 | /// separated by newlines. Newlines can be represented either as a carriage return/line feed 440 | /// pair (`\r\n`) or as a single line feed character (`\n`). 441 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 442 | StreetAddress(String) 443 | ]; 444 | 445 | new_type![ 446 | /// Locally unique and never reassigned identifier within the Issuer for the End-User, which is 447 | /// intended to be consumed by the client application. 448 | #[derive(Deserialize, Hash, Ord, PartialOrd, Serialize)] 449 | SubjectIdentifier(String) 450 | ]; 451 | 452 | new_url_type![ 453 | /// URL for the relying party's Terms of Service. 454 | ToSUrl 455 | ]; 456 | -------------------------------------------------------------------------------- /src/jwt/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{ 2 | CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, 3 | CoreRsaPrivateSigningKey, 4 | }; 5 | use crate::jwt::{ 6 | InvalidJsonWebTokenTypeError, JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, 7 | JsonWebTokenJsonPayloadSerde, JsonWebTokenPayloadSerde, 8 | }; 9 | use crate::{JsonWebKeyId, JsonWebTokenType}; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | 13 | use std::string::ToString; 14 | 15 | type CoreAlgorithm = 16 | JsonWebTokenAlgorithm; 17 | 18 | pub const TEST_JWT: &str = 19 | "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9.SXTigJlzIGEgZ\ 20 | GFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0aGU\ 21 | gcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlc\ 22 | mUgeW91IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4.MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e\ 23 | 5CZ5NlKtainoFmKZopdHM1O2U4mwzJdQx996ivp83xuglII7PNDi84wnB-BDkoBwA78185hX-Es4JIwmDLJK3l\ 24 | fWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9eRo4QPocSadnHXFxnt8Is9UzpERV\ 25 | 0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4uOZDnd6QZWJushZ41\ 26 | Axf_fcIe8u9ipH84ogoree7vjbU5y18kDquDg"; 27 | 28 | const TEST_JWT_PAYLOAD: &str = "It\u{2019}s a dangerous business, Frodo, going out your \ 29 | door. You step onto the road, and if you don't keep your feet, \ 30 | there\u{2019}s no knowing where you might be swept off \ 31 | to."; 32 | 33 | pub const TEST_RSA_PUB_KEY: &str = "{ 34 | \"kty\": \"RSA\", 35 | \"kid\": \"bilbo.baggins@hobbiton.example\", 36 | \"use\": \"sig\", 37 | \"n\": \"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT\ 38 | -O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqV\ 39 | wGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-\ 40 | oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde\ 41 | 3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuC\ 42 | LqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5g\ 43 | HdrNP5zw\", 44 | \"e\": \"AQAB\" 45 | }"; 46 | 47 | pub const TEST_ED_PUB_KEY_ED25519: &str = r#"{ 48 | "kty": "OKP", 49 | "use": "sig", 50 | "alg": "EdDSA", 51 | "crv": "Ed25519", 52 | "x": "sfliRRhciU_d5qsuC5Vcydi-t8bRfxTg_4qulVatW4A" 53 | }"#; 54 | 55 | pub const TEST_EC_PUB_KEY_P256: &str = r#"{ 56 | "kty": "EC", 57 | "kid": "bilbo.baggins@hobbiton.example", 58 | "use": "sig", 59 | "crv": "P-256", 60 | "x": "t6PHivOTggpaX9lkMkis2p8kMhy-CktJAFTz6atReZw", 61 | "y": "ODobXupKlD0DeM1yRd7bX4XFNBO1HOgCT1UCu0KY3lc" 62 | }"#; 63 | pub const TEST_EC_PUB_KEY_P384: &str = r#"{ 64 | "kty": "EC", 65 | "kid": "bilbo.baggins@hobbiton.example", 66 | "use": "sig", 67 | "crv" : "P-384", 68 | "x": "9ywsUbxX59kJXFRiWHcx97wRKNiF8Hc9F5wI08n8h2ek_qAl0veEc36k1Qz6KLiL", 69 | "y": "6PWlqjRbaV7V8ohDscM243IneuLZmxDGLiGNA1w69fQhEDsvZtKLUQ5KiHLgR3op" 70 | }"#; 71 | 72 | // This is the PEM form of the test private key from: 73 | // https://tools.ietf.org/html/rfc7520#section-3.4 74 | pub const TEST_RSA_PRIV_KEY: &str = "-----BEGIN RSA PRIVATE KEY-----\n\ 75 | MIIEowIBAAKCAQEAn4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8/KuKPEHLd4\n\ 76 | rHVTeT+O+XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz/AJmSCpMaJMRBSFKrKb2wqVwG\n\ 77 | U/NsYOYL+QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj+oBHqFEHYpP\n\ 78 | e7Tpe+OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzw\n\ 79 | OHrtIQbS0FVbb9k3+tVTU4fg/3L/vniUFAKwuCLqKnS2BYwdq/mzSnbLY7h/qixo\n\ 80 | R7jig3//kRhuaxwUkRz5iaiQkqgc5gHdrNP5zwIDAQABAoIBAG1lAvQfhBUSKPJK\n\ 81 | Rn4dGbshj7zDSr2FjbQf4pIh/ZNtHk/jtavyO/HomZKV8V0NFExLNi7DUUvvLiW7\n\ 82 | 0PgNYq5MDEjJCtSd10xoHa4QpLvYEZXWO7DQPwCmRofkOutf+NqyDS0QnvFvp2d+\n\ 83 | Lov6jn5C5yvUFgw6qWiLAPmzMFlkgxbtjFAWMJB0zBMy2BqjntOJ6KnqtYRMQUxw\n\ 84 | TgXZDF4rhYVKtQVOpfg6hIlsaoPNrF7dofizJ099OOgDmCaEYqM++bUlEHxgrIVk\n\ 85 | wZz+bg43dfJCocr9O5YX0iXaz3TOT5cpdtYbBX+C/5hwrqBWru4HbD3xz8cY1TnD\n\ 86 | qQa0M8ECgYEA3Slxg/DwTXJcb6095RoXygQCAZ5RnAvZlno1yhHtnUex/fp7AZ/9\n\ 87 | nRaO7HX/+SFfGQeutao2TDjDAWU4Vupk8rw9JR0AzZ0N2fvuIAmr/WCsmGpeNqQn\n\ 88 | ev1T7IyEsnh8UMt+n5CafhkikzhEsrmndH6LxOrvRJlsPp6Zv8bUq0kCgYEAuKE2\n\ 89 | dh+cTf6ERF4k4e/jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR/cu0Dm1MZwW\n\ 90 | mtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoB\n\ 91 | vyY898EXvRD+hdqRxHlSqAZ192zB3pVFJ0s7pFcCgYAHw9W9eS8muPYv4ZhDu/fL\n\ 92 | 2vorDmD1JqFcHCxZTOnX1NWWAj5hXzmrU0hvWvFC0P4ixddHf5Nqd6+5E9G3k4E5\n\ 93 | 2IwZCnylu3bqCWNh8pT8T3Gf5FQsfPT5530T2BcsoPhUaeCnP499D+rb2mTnFYeg\n\ 94 | mnTT1B/Ue8KGLFFfn16GKQKBgAiw5gxnbocpXPaO6/OKxFFZ+6c0OjxfN2PogWce\n\ 95 | TU/k6ZzmShdaRKwDFXisxRJeNQ5Rx6qgS0jNFtbDhW8E8WFmQ5urCOqIOYk28EBi\n\ 96 | At4JySm4v+5P7yYBh8B8YD2l9j57z/s8hJAxEbn/q8uHP2ddQqvQKgtsni+pHSk9\n\ 97 | XGBfAoGBANz4qr10DdM8DHhPrAb2YItvPVz/VwkBd1Vqj8zCpyIEKe/07oKOvjWQ\n\ 98 | SgkLDH9x2hBgY01SbP43CvPk0V72invu2TGkI/FXwXWJLLG7tDSgw4YyfhrYrHmg\n\ 99 | 1Vre3XB9HH8MYBVB6UIexaAq4xSeoemRKTBesZro7OKjKT8/GmiO\n\ 100 | -----END RSA PRIVATE KEY-----"; 101 | 102 | #[test] 103 | fn test_jwt_algorithm_deserialization() { 104 | assert_eq!( 105 | serde_json::from_str::("\"A128CBC-HS256\"").expect("failed to deserialize"), 106 | JsonWebTokenAlgorithm::Encryption(CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256), 107 | ); 108 | assert_eq!( 109 | serde_json::from_str::("\"A128GCM\"").expect("failed to deserialize"), 110 | JsonWebTokenAlgorithm::Encryption(CoreJweContentEncryptionAlgorithm::Aes128Gcm), 111 | ); 112 | assert_eq!( 113 | serde_json::from_str::("\"HS256\"").expect("failed to deserialize"), 114 | JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::HmacSha256), 115 | ); 116 | assert_eq!( 117 | serde_json::from_str::("\"RS256\"").expect("failed to deserialize"), 118 | JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256), 119 | ); 120 | assert_eq!( 121 | serde_json::from_str::("\"none\"").expect("failed to deserialize"), 122 | JsonWebTokenAlgorithm::None, 123 | ); 124 | 125 | serde_json::from_str::("\"invalid\"") 126 | .expect_err("deserialization should have failed"); 127 | } 128 | 129 | #[test] 130 | fn test_jwt_algorithm_serialization() { 131 | assert_eq!( 132 | serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( 133 | CoreJweContentEncryptionAlgorithm::Aes128CbcHmacSha256 134 | )) 135 | .expect("failed to serialize"), 136 | "\"A128CBC-HS256\"", 137 | ); 138 | assert_eq!( 139 | serde_json::to_string::(&JsonWebTokenAlgorithm::Encryption( 140 | CoreJweContentEncryptionAlgorithm::Aes128Gcm 141 | )) 142 | .expect("failed to serialize"), 143 | "\"A128GCM\"", 144 | ); 145 | assert_eq!( 146 | serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( 147 | CoreJwsSigningAlgorithm::HmacSha256 148 | )) 149 | .expect("failed to serialize"), 150 | "\"HS256\"", 151 | ); 152 | assert_eq!( 153 | serde_json::to_string::(&JsonWebTokenAlgorithm::Signature( 154 | CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256 155 | )) 156 | .expect("failed to serialize"), 157 | "\"RS256\"", 158 | ); 159 | assert_eq!( 160 | serde_json::to_string::(&JsonWebTokenAlgorithm::None) 161 | .expect("failed to serialize"), 162 | "\"none\"", 163 | ); 164 | } 165 | 166 | #[derive(Clone, Debug)] 167 | pub struct JsonWebTokenStringPayloadSerde; 168 | impl JsonWebTokenPayloadSerde for JsonWebTokenStringPayloadSerde { 169 | fn deserialize(payload: &[u8]) -> Result { 170 | Ok(String::from_utf8(payload.to_owned()).unwrap()) 171 | } 172 | fn serialize(payload: &String) -> Result { 173 | Ok(payload.to_string()) 174 | } 175 | } 176 | 177 | #[test] 178 | fn test_jwt_basic() { 179 | fn verify_jwt(jwt_access: A, key: &CoreJsonWebKey, expected_payload: &str) 180 | where 181 | A: JsonWebTokenAccess, 182 | A::ReturnType: ToString, 183 | { 184 | { 185 | let header = jwt_access.unverified_header(); 186 | assert_eq!( 187 | header.alg, 188 | JsonWebTokenAlgorithm::Signature(CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256) 189 | ); 190 | assert_eq!(header.crit, None); 191 | assert_eq!(header.cty, None); 192 | assert_eq!( 193 | header.kid, 194 | Some(JsonWebKeyId::new( 195 | "bilbo.baggins@hobbiton.example".to_string() 196 | )) 197 | ); 198 | assert_eq!(header.typ, None); 199 | } 200 | assert_eq!(jwt_access.unverified_payload_ref(), expected_payload); 201 | 202 | assert_eq!( 203 | jwt_access 204 | .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, key) 205 | .expect("failed to validate payload") 206 | .to_string(), 207 | expected_payload 208 | ); 209 | } 210 | 211 | let key: CoreJsonWebKey = 212 | serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); 213 | 214 | let jwt: JsonWebToken< 215 | CoreJweContentEncryptionAlgorithm, 216 | CoreJwsSigningAlgorithm, 217 | String, 218 | JsonWebTokenStringPayloadSerde, 219 | > = serde_json::from_value(serde_json::Value::String(TEST_JWT.to_string())) 220 | .expect("failed to deserialize"); 221 | 222 | assert_eq!( 223 | serde_json::to_value(&jwt).expect("failed to serialize"), 224 | serde_json::Value::String(TEST_JWT.to_string()) 225 | ); 226 | 227 | verify_jwt(&jwt, &key, TEST_JWT_PAYLOAD); 228 | assert_eq!((&jwt).unverified_payload(), TEST_JWT_PAYLOAD); 229 | 230 | verify_jwt(jwt, &key, TEST_JWT_PAYLOAD); 231 | } 232 | 233 | #[test] 234 | fn test_new_jwt() { 235 | let signing_key = CoreRsaPrivateSigningKey::from_pem( 236 | TEST_RSA_PRIV_KEY, 237 | Some(JsonWebKeyId::new( 238 | "bilbo.baggins@hobbiton.example".to_string(), 239 | )), 240 | ) 241 | .unwrap(); 242 | let new_jwt = JsonWebToken::< 243 | CoreJweContentEncryptionAlgorithm, 244 | _, 245 | _, 246 | JsonWebTokenStringPayloadSerde, 247 | >::new( 248 | TEST_JWT_PAYLOAD.to_owned(), 249 | &signing_key, 250 | &CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, 251 | ) 252 | .unwrap(); 253 | assert_eq!( 254 | serde_json::to_value(new_jwt).expect("failed to serialize"), 255 | serde_json::Value::String(TEST_JWT.to_string()) 256 | ); 257 | } 258 | 259 | #[test] 260 | fn test_invalid_signature() { 261 | let corrupted_jwt_str = TEST_JWT 262 | .to_string() 263 | .chars() 264 | .take(TEST_JWT.len() - 1) 265 | .collect::() 266 | + "f"; 267 | let jwt: JsonWebToken< 268 | CoreJweContentEncryptionAlgorithm, 269 | CoreJwsSigningAlgorithm, 270 | String, 271 | JsonWebTokenStringPayloadSerde, 272 | > = serde_json::from_value(serde_json::Value::String(corrupted_jwt_str)) 273 | .expect("failed to deserialize"); 274 | let key: CoreJsonWebKey = 275 | serde_json::from_str(TEST_RSA_PUB_KEY).expect("deserialization failed"); 276 | 277 | // JsonWebTokenAccess for reference. 278 | (&jwt) 279 | .payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) 280 | .expect_err("signature verification should have failed"); 281 | 282 | // JsonWebTokenAccess for owned value. 283 | jwt.payload(&CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, &key) 284 | .expect_err("signature verification should have failed"); 285 | } 286 | 287 | #[test] 288 | fn test_invalid_deserialization() { 289 | #[derive(Debug, Deserialize, Serialize)] 290 | struct TestPayload { 291 | foo: String, 292 | } 293 | 294 | fn expect_deserialization_err>(jwt_str: I, pattern: &str) { 295 | let err = serde_json::from_value::< 296 | JsonWebToken< 297 | CoreJweContentEncryptionAlgorithm, 298 | CoreJwsSigningAlgorithm, 299 | TestPayload, 300 | JsonWebTokenJsonPayloadSerde, 301 | >, 302 | >(serde_json::Value::String(jwt_str.into())) 303 | .expect_err("deserialization should have failed"); 304 | 305 | assert!( 306 | err.to_string().contains(pattern), 307 | "Error `{}` must contain string `{}`", 308 | err, 309 | pattern, 310 | ); 311 | } 312 | 313 | // Too many dots 314 | expect_deserialization_err("a.b.c.d", "found 4 parts (expected 3)"); 315 | 316 | // Invalid header base64 317 | expect_deserialization_err("a!.b.c", "Invalid base64url header encoding"); 318 | 319 | // Invalid header utf-8 (after base64 decoding) 320 | expect_deserialization_err("gA.b.c", "Error(\"expected value\", line: 1, column: 1)"); 321 | 322 | // Invalid header JSON 323 | expect_deserialization_err("bm90X2pzb24.b.c", "Failed to parse header JSON"); 324 | 325 | let valid_header = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9"; 326 | 327 | // Invalid payload base64 328 | expect_deserialization_err( 329 | format!("{}.b!.c", valid_header), 330 | "Invalid base64url payload encoding", 331 | ); 332 | 333 | // Invalid payload utf-8 (after base64 decoding) 334 | expect_deserialization_err( 335 | format!("{}.gA.c", valid_header), 336 | "Error(\"expected value\", line: 1, column: 1)", 337 | ); 338 | 339 | // Invalid payload JSON 340 | expect_deserialization_err( 341 | format!("{}.bm90X2pzb24.c", valid_header), 342 | "Failed to parse payload JSON", 343 | ); 344 | 345 | let valid_body = "eyJmb28iOiAiYmFyIn0"; 346 | 347 | // Invalid signature base64 348 | expect_deserialization_err( 349 | format!("{}.{}.c!", valid_header, valid_body), 350 | "Invalid base64url signature encoding", 351 | ); 352 | 353 | let deserialized = serde_json::from_value::< 354 | JsonWebToken< 355 | CoreJweContentEncryptionAlgorithm, 356 | CoreJwsSigningAlgorithm, 357 | TestPayload, 358 | JsonWebTokenJsonPayloadSerde, 359 | >, 360 | >(serde_json::Value::String(format!( 361 | "{}.{}.e2FiY30", 362 | valid_header, valid_body 363 | ))) 364 | .expect("failed to deserialize"); 365 | assert_eq!(deserialized.unverified_payload().foo, "bar"); 366 | } 367 | 368 | #[test] 369 | fn test_json_web_token_type_normalization() { 370 | fn assert_token_type_normalization( 371 | jwt_type_string: &str, 372 | expected_normalized_jwt_type_string: &str, 373 | ) -> Result<(), InvalidJsonWebTokenTypeError> { 374 | let jwt_type = JsonWebTokenType::new(jwt_type_string.to_string()); 375 | let normalized_jwt_type = jwt_type.normalize()?; 376 | 377 | assert_eq!(*normalized_jwt_type, expected_normalized_jwt_type_string); 378 | Ok(()) 379 | } 380 | 381 | assert_token_type_normalization("jwt", "application/jwt").unwrap(); 382 | assert_token_type_normalization("jwt;arg=some", "application/jwt;arg=some").unwrap(); 383 | assert!(assert_token_type_normalization("jwt;arg=some/other", "").is_err()); 384 | assert!(assert_token_type_normalization("/jwt;arg=some/other", "").is_err()); 385 | assert!(assert_token_type_normalization("application/;arg=some/other", "").is_err()); 386 | assert_token_type_normalization("application/jwt", "application/jwt").unwrap(); 387 | assert_token_type_normalization( 388 | "application/jwt;arg=some/other", 389 | "application/jwt;arg=some/other", 390 | ) 391 | .unwrap(); 392 | assert_token_type_normalization("special/type", "special/type").unwrap(); 393 | assert_token_type_normalization("special/type;arg=some", "special/type;arg=some").unwrap(); 394 | assert_token_type_normalization("s/t;arg=some/o", "s/t;arg=some/o").unwrap(); 395 | } 396 | -------------------------------------------------------------------------------- /src/id_token/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{deserialize_string_or_vec, FilteredFlatten, Timestamp}; 2 | use crate::jwt::JsonWebTokenAccess; 3 | use crate::jwt::{JsonWebTokenError, JsonWebTokenJsonPayloadSerde}; 4 | use crate::types::jwk::JwsSigningAlgorithm; 5 | use crate::{ 6 | AccessToken, AccessTokenHash, AdditionalClaims, AddressClaim, Audience, AudiencesClaim, 7 | AuthenticationContextClass, AuthenticationMethodReference, AuthorizationCode, 8 | AuthorizationCodeHash, ClaimsVerificationError, ClientId, EndUserBirthday, EndUserEmail, 9 | EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname, 10 | EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername, 11 | EndUserWebsiteUrl, ExtraTokenFields, GenderClaim, IdTokenVerifier, IssuerClaim, IssuerUrl, 12 | JsonWebKey, JsonWebToken, JsonWebTokenAlgorithm, JweContentEncryptionAlgorithm, LanguageTag, 13 | LocalizedClaim, Nonce, NonceVerifier, PrivateSigningKey, SignatureVerificationError, 14 | StandardClaims, SubjectIdentifier, 15 | }; 16 | 17 | use chrono::{DateTime, Utc}; 18 | use serde::{Deserialize, Serialize}; 19 | use serde_json::Value; 20 | use serde_with::{serde_as, skip_serializing_none}; 21 | 22 | use std::fmt::Debug; 23 | use std::str::FromStr; 24 | 25 | #[cfg(test)] 26 | mod tests; 27 | 28 | // This wrapper layer exists instead of directly verifying the JWT and returning the claims so that 29 | // we can pass it around and easily access a serialized JWT representation of it (e.g., for passing 30 | // to the authorization endpoint as an id_token_hint). 31 | /// OpenID Connect ID token. 32 | #[cfg_attr( 33 | any(test, feature = "timing-resistant-secret-traits"), 34 | derive(PartialEq) 35 | )] 36 | #[derive(Clone, Debug, Deserialize, Serialize)] 37 | pub struct IdToken< 38 | AC: AdditionalClaims, 39 | GC: GenderClaim, 40 | JE: JweContentEncryptionAlgorithm, 41 | JS: JwsSigningAlgorithm, 42 | >( 43 | #[serde(bound = "AC: AdditionalClaims")] 44 | JsonWebToken, JsonWebTokenJsonPayloadSerde>, 45 | ); 46 | 47 | impl FromStr for IdToken 48 | where 49 | AC: AdditionalClaims, 50 | GC: GenderClaim, 51 | JE: JweContentEncryptionAlgorithm, 52 | JS: JwsSigningAlgorithm, 53 | { 54 | type Err = serde_json::Error; 55 | fn from_str(s: &str) -> Result { 56 | serde_json::from_value(Value::String(s.to_string())) 57 | } 58 | } 59 | 60 | impl IdToken 61 | where 62 | AC: AdditionalClaims, 63 | GC: GenderClaim, 64 | JE: JweContentEncryptionAlgorithm, 65 | JS: JwsSigningAlgorithm, 66 | { 67 | /// Initializes an ID token with the specified claims, signed using the given signing key and 68 | /// algorithm. 69 | /// 70 | /// If an `access_token` and/or `code` are provided, this method sets the `at_hash` and/or 71 | /// `c_hash` claims using the given signing algorithm, respectively. Otherwise, those claims are 72 | /// unchanged from the values specified in `claims`. 73 | pub fn new( 74 | claims: IdTokenClaims, 75 | signing_key: &S, 76 | alg: JS, 77 | access_token: Option<&AccessToken>, 78 | code: Option<&AuthorizationCode>, 79 | ) -> Result 80 | where 81 | S: PrivateSigningKey, 82 | ::VerificationKey: JsonWebKey, 83 | { 84 | let verification_key = signing_key.as_verification_key(); 85 | let at_hash = access_token 86 | .map(|at| { 87 | AccessTokenHash::from_token(at, &alg, &verification_key) 88 | .map_err(JsonWebTokenError::SigningError) 89 | }) 90 | .transpose()? 91 | .or_else(|| claims.access_token_hash.clone()); 92 | let c_hash = code 93 | .map(|c| { 94 | AuthorizationCodeHash::from_code(c, &alg, &verification_key) 95 | .map_err(JsonWebTokenError::SigningError) 96 | }) 97 | .transpose()? 98 | .or_else(|| claims.code_hash.clone()); 99 | 100 | JsonWebToken::new( 101 | IdTokenClaims { 102 | access_token_hash: at_hash, 103 | code_hash: c_hash, 104 | ..claims 105 | }, 106 | signing_key, 107 | &alg, 108 | ) 109 | .map(Self) 110 | } 111 | 112 | /// Verifies and returns a reference to the ID token claims. 113 | pub fn claims<'a, K, N>( 114 | &'a self, 115 | verifier: &IdTokenVerifier, 116 | nonce_verifier: N, 117 | ) -> Result<&'a IdTokenClaims, ClaimsVerificationError> 118 | where 119 | K: JsonWebKey, 120 | N: NonceVerifier, 121 | { 122 | verifier.verified_claims(&self.0, nonce_verifier) 123 | } 124 | 125 | /// Verifies and returns the ID token claims. 126 | pub fn into_claims( 127 | self, 128 | verifier: &IdTokenVerifier, 129 | nonce_verifier: N, 130 | ) -> Result, ClaimsVerificationError> 131 | where 132 | K: JsonWebKey, 133 | N: NonceVerifier, 134 | { 135 | verifier.verified_claims_owned(self.0, nonce_verifier) 136 | } 137 | 138 | /// Returns the [`JwsSigningAlgorithm`] used to sign this ID token. 139 | /// 140 | /// This function returns an error if the token is unsigned or utilizes JSON Web Encryption 141 | /// (JWE). 142 | pub fn signing_alg(&self) -> Result<&JS, SignatureVerificationError> { 143 | match self.0.unverified_header().alg { 144 | JsonWebTokenAlgorithm::Signature(ref signing_alg) => Ok(signing_alg), 145 | JsonWebTokenAlgorithm::Encryption(ref other) => { 146 | Err(SignatureVerificationError::UnsupportedAlg( 147 | serde_plain::to_string(other).unwrap_or_else(|err| { 148 | panic!( 149 | "encryption alg {:?} failed to serialize to a string: {}", 150 | other, err 151 | ) 152 | }), 153 | )) 154 | } 155 | JsonWebTokenAlgorithm::None => Err(SignatureVerificationError::NoSignature), 156 | } 157 | } 158 | 159 | /// Returns the [`JsonWebKey`] usable for verifying this ID token's JSON Web Signature. 160 | /// 161 | /// This function returns an error if the token has no signature or a corresponding key cannot 162 | /// be found. 163 | pub fn signing_key<'s, K>( 164 | &self, 165 | verifier: &'s IdTokenVerifier<'s, K>, 166 | ) -> Result<&'s K, SignatureVerificationError> 167 | where 168 | K: JsonWebKey, 169 | { 170 | verifier 171 | .jwt_verifier 172 | .signing_key(self.0.unverified_header().kid.as_ref(), self.signing_alg()?) 173 | } 174 | } 175 | impl ToString for IdToken 176 | where 177 | AC: AdditionalClaims, 178 | GC: GenderClaim, 179 | JE: JweContentEncryptionAlgorithm, 180 | JS: JwsSigningAlgorithm, 181 | { 182 | fn to_string(&self) -> String { 183 | serde_json::to_value(self) 184 | // This should never arise, since we're just asking serde_json to serialize the 185 | // signing input concatenated with the signature, both of which are precomputed. 186 | .expect("ID token serialization failed") 187 | .as_str() 188 | // This should also never arise, since our IdToken serializer always calls serialize_str 189 | .expect("ID token serializer did not produce a str") 190 | .to_owned() 191 | } 192 | } 193 | 194 | /// OpenID Connect ID token claims. 195 | #[cfg_attr( 196 | any(test, feature = "timing-resistant-secret-traits"), 197 | derive(PartialEq) 198 | )] 199 | #[serde_as] 200 | #[skip_serializing_none] 201 | #[derive(Clone, Debug, Deserialize, Serialize)] 202 | pub struct IdTokenClaims 203 | where 204 | AC: AdditionalClaims, 205 | GC: GenderClaim, 206 | { 207 | #[serde(rename = "iss")] 208 | issuer: IssuerUrl, 209 | // We always serialize as an array, which is valid according to the spec. This sets the 210 | // 'default' attribute to be compatible with non-spec compliant OIDC providers that omit this 211 | // field. 212 | #[serde( 213 | default, 214 | rename = "aud", 215 | deserialize_with = "deserialize_string_or_vec" 216 | )] 217 | audiences: Vec, 218 | #[serde_as(as = "Timestamp")] 219 | #[serde(rename = "exp")] 220 | expiration: DateTime, 221 | #[serde_as(as = "Timestamp")] 222 | #[serde(rename = "iat")] 223 | issue_time: DateTime, 224 | #[serde_as(as = "Option")] 225 | auth_time: Option>, 226 | nonce: Option, 227 | #[serde(rename = "acr")] 228 | auth_context_ref: Option, 229 | #[serde(rename = "amr")] 230 | auth_method_refs: Option>, 231 | #[serde(rename = "azp")] 232 | authorized_party: Option, 233 | #[serde(rename = "at_hash")] 234 | access_token_hash: Option, 235 | #[serde(rename = "c_hash")] 236 | code_hash: Option, 237 | 238 | #[serde(bound = "GC: GenderClaim")] 239 | #[serde(flatten)] 240 | standard_claims: StandardClaims, 241 | 242 | #[serde(bound = "AC: AdditionalClaims")] 243 | #[serde(flatten)] 244 | additional_claims: FilteredFlatten, AC>, 245 | } 246 | impl IdTokenClaims 247 | where 248 | AC: AdditionalClaims, 249 | GC: GenderClaim, 250 | { 251 | /// Initializes new ID token claims. 252 | pub fn new( 253 | issuer: IssuerUrl, 254 | audiences: Vec, 255 | expiration: DateTime, 256 | issue_time: DateTime, 257 | standard_claims: StandardClaims, 258 | additional_claims: AC, 259 | ) -> Self { 260 | Self { 261 | issuer, 262 | audiences, 263 | expiration, 264 | issue_time, 265 | auth_time: None, 266 | nonce: None, 267 | auth_context_ref: None, 268 | auth_method_refs: None, 269 | authorized_party: None, 270 | access_token_hash: None, 271 | code_hash: None, 272 | standard_claims, 273 | additional_claims: additional_claims.into(), 274 | } 275 | } 276 | 277 | field_getters_setters![ 278 | pub self [self] ["claim"] { 279 | set_issuer -> issuer[IssuerUrl] ["iss"], 280 | set_audiences -> audiences[Vec] ["aud"], 281 | set_expiration -> expiration[DateTime] ["exp"], 282 | set_issue_time -> issue_time[DateTime] ["iat"], 283 | set_auth_time -> auth_time[Option>], 284 | set_nonce -> nonce[Option], 285 | set_auth_context_ref -> auth_context_ref[Option] ["acr"], 286 | set_auth_method_refs -> auth_method_refs[Option>] ["amr"], 287 | set_authorized_party -> authorized_party[Option] ["azp"], 288 | set_access_token_hash -> access_token_hash[Option] ["at_hash"], 289 | set_code_hash -> code_hash[Option] ["c_hash"], 290 | } 291 | ]; 292 | 293 | /// Returns the `sub` claim. 294 | pub fn subject(&self) -> &SubjectIdentifier { 295 | &self.standard_claims.sub 296 | } 297 | /// Sets the `sub` claim. 298 | pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self { 299 | self.standard_claims.sub = subject; 300 | self 301 | } 302 | 303 | field_getters_setters![ 304 | pub self [self.standard_claims] ["claim"] { 305 | set_name -> name[Option>], 306 | set_given_name -> given_name[Option>], 307 | set_family_name -> 308 | family_name[Option>], 309 | set_middle_name -> 310 | middle_name[Option>], 311 | set_nickname -> nickname[Option>], 312 | set_preferred_username -> preferred_username[Option], 313 | set_profile -> profile[Option>], 314 | set_picture -> picture[Option>], 315 | set_website -> website[Option>], 316 | set_email -> email[Option], 317 | set_email_verified -> email_verified[Option], 318 | set_gender -> gender[Option], 319 | set_birthday -> birthday[Option], 320 | set_birthdate -> birthdate[Option], 321 | set_zoneinfo -> zoneinfo[Option], 322 | set_locale -> locale[Option], 323 | set_phone_number -> phone_number[Option], 324 | set_phone_number_verified -> phone_number_verified[Option], 325 | set_address -> address[Option], 326 | set_updated_at -> updated_at[Option>], 327 | } 328 | ]; 329 | 330 | /// Returns additional ID token claims. 331 | pub fn additional_claims(&self) -> &AC { 332 | self.additional_claims.as_ref() 333 | } 334 | /// Returns mutable additional ID token claims. 335 | pub fn additional_claims_mut(&mut self) -> &mut AC { 336 | self.additional_claims.as_mut() 337 | } 338 | } 339 | impl AudiencesClaim for IdTokenClaims 340 | where 341 | AC: AdditionalClaims, 342 | GC: GenderClaim, 343 | { 344 | fn audiences(&self) -> Option<&Vec> { 345 | Some(IdTokenClaims::audiences(self)) 346 | } 347 | } 348 | impl<'a, AC, GC> AudiencesClaim for &'a IdTokenClaims 349 | where 350 | AC: AdditionalClaims, 351 | GC: GenderClaim, 352 | { 353 | fn audiences(&self) -> Option<&Vec> { 354 | Some(IdTokenClaims::audiences(self)) 355 | } 356 | } 357 | impl IssuerClaim for IdTokenClaims 358 | where 359 | AC: AdditionalClaims, 360 | GC: GenderClaim, 361 | { 362 | fn issuer(&self) -> Option<&IssuerUrl> { 363 | Some(IdTokenClaims::issuer(self)) 364 | } 365 | } 366 | impl<'a, AC, GC> IssuerClaim for &'a IdTokenClaims 367 | where 368 | AC: AdditionalClaims, 369 | GC: GenderClaim, 370 | { 371 | fn issuer(&self) -> Option<&IssuerUrl> { 372 | Some(IdTokenClaims::issuer(self)) 373 | } 374 | } 375 | 376 | /// Extends the base OAuth2 token response with an ID token. 377 | #[cfg_attr( 378 | any(test, feature = "timing-resistant-secret-traits"), 379 | derive(PartialEq) 380 | )] 381 | #[derive(Clone, Debug, Deserialize, Serialize)] 382 | pub struct IdTokenFields 383 | where 384 | AC: AdditionalClaims, 385 | EF: ExtraTokenFields, 386 | GC: GenderClaim, 387 | JE: JweContentEncryptionAlgorithm, 388 | JS: JwsSigningAlgorithm, 389 | { 390 | #[serde(bound = "AC: AdditionalClaims")] 391 | id_token: Option>, 392 | #[serde(bound = "EF: ExtraTokenFields", flatten)] 393 | extra_fields: EF, 394 | } 395 | impl IdTokenFields 396 | where 397 | AC: AdditionalClaims, 398 | EF: ExtraTokenFields, 399 | GC: GenderClaim, 400 | JE: JweContentEncryptionAlgorithm, 401 | JS: JwsSigningAlgorithm, 402 | { 403 | /// Initializes new ID token fields containing the specified [`IdToken`] and extra fields. 404 | pub fn new(id_token: Option>, extra_fields: EF) -> Self { 405 | Self { 406 | id_token, 407 | extra_fields, 408 | } 409 | } 410 | 411 | /// Returns the [`IdToken`] contained in the OAuth2 token response. 412 | pub fn id_token(&self) -> Option<&IdToken> { 413 | self.id_token.as_ref() 414 | } 415 | /// Returns the extra fields contained in the OAuth2 token response. 416 | pub fn extra_fields(&self) -> &EF { 417 | &self.extra_fields 418 | } 419 | } 420 | impl ExtraTokenFields for IdTokenFields 421 | where 422 | AC: AdditionalClaims, 423 | EF: ExtraTokenFields, 424 | GC: GenderClaim, 425 | JE: JweContentEncryptionAlgorithm, 426 | JS: JwsSigningAlgorithm, 427 | { 428 | } 429 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Upgrading from 3.x to 4.x 4 | 5 | The 4.0 release includes breaking changes to address several long-standing API issues, along with 6 | a few minor improvements. Consider following the tips below to help ensure a smooth upgrade 7 | process. This document is not exhaustive but covers the breaking changes most likely to affect 8 | typical uses of this crate. 9 | 10 | ### Add typestate generic types to `Client` 11 | 12 | Each auth flow depends on one or more server endpoints. For example, the 13 | authorization code flow depends on both an authorization endpoint and a token endpoint, while the 14 | client credentials flow only depends on a token endpoint. Previously, it was possible to instantiate 15 | a `Client` without a token endpoint and then attempt to use an auth flow that required a token 16 | endpoint, leading to errors at runtime. Also, the authorization endpoint was always required, even 17 | for auth flows that do not use it. 18 | 19 | In the 4.0 release, all endpoints are optional. 20 | [Typestates](https://cliffle.com/blog/rust-typestate/) are used to statically track, at compile 21 | time, which endpoints' setters (e.g., `set_auth_uri()`) have been called. Auth flows that depend on 22 | an endpoint cannot be used without first calling the corresponding setter, which is enforced by the 23 | compiler's type checker. This guarantees that certain errors will not arise at runtime. 24 | 25 | When using [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) 26 | (i.e., `Client::from_provider_metadata()`), 27 | each discoverable endpoint is set to a conditional typestate (`EndpointMaybeSet`). This is because 28 | it cannot be determined at compile time whether each of these endpoints will be returned by the 29 | OpenID Provider. When the conditional typestate is set, endpoints can be used via fallible methods 30 | that return `Err(ConfigurationError::MissingUrl(_))` if an endpoint has not been set. 31 | 32 | There are three possible typestates, each implementing the `EndpointState` trait: 33 | * `EndpointNotSet`: the corresponding endpoint has **not** been set and cannot be used. 34 | * `EndpointSet`: the corresponding endpoint **has** been set and is ready to be used. 35 | * `EndpointMaybeSet`: the corresponding endpoint **may have** been set and can be used via fallible 36 | methods that return `Result<_, ConfigurationError>`. 37 | 38 | The following code changes are required to support the new interface: 39 | 1. Update calls to 40 | [`Client::new()`](https://docs.rs/openidconnect/latest/openidconnect/struct.Client.html#method.new) 41 | to use the three-argument constructor (which accepts only a `ClientId`, `IssuerUrl`, and 42 | `JsonWebKeySet`). Use the `set_auth_uri()`, `set_token_uri()`, `set_user_info_url()`, and 43 | `set_client_secret()` methods to set the authorization endpoint, token endpoint, user info 44 | endpoint, and client secret, respectively, if applicable to your application's auth flows. 45 | 2. If using `Client::from_provider_metadata()`, update call sites that use each auth flow 46 | (e.g., `Client::exchange_code()`) to handle the possibility of a `ConfigurationError` if the 47 | corresponding endpoint was not specified in the provider metadata. 48 | 3. If required by your usage of the `Client` or `CoreClient` types (i.e., if you see related 49 | compiler errors), add the following generic parameters: 50 | ```rust 51 | HasAuthUrl: EndpointState, 52 | HasDeviceAuthUrl: EndpointState, 53 | HasIntrospectionUrl: EndpointState, 54 | HasRevocationUrl: EndpointState, 55 | HasTokenUrl: EndpointState, 56 | HasUserInfoUrl: EndpointState, 57 | ``` 58 | For example, if you store a `CoreClient` within another data type, you may need to annotate it as 59 | `CoreClient` 60 | if it has both an authorization endpoint and a token endpoint set. Compiler error messages will 61 | likely guide you to the appropriate combination of typestates. 62 | 63 | If, instead of using `CoreClient`, you are directly using `Client` with a different set of type 64 | parameters, you will need to append the five generic typestate parameters. For example, replace: 65 | ```rust 66 | type SpecialClient = Client< 67 | EmptyAdditionalClaims, 68 | CoreAuthDisplay, 69 | CoreGenderClaim, 70 | CoreJweContentEncryptionAlgorithm, 71 | CoreJwsSigningAlgorithm, 72 | CoreJsonWebKeyType, 73 | CoreJsonWebKeyUse, 74 | CoreJsonWebKey, 75 | CoreAuthPrompt, 76 | StandardErrorResponse, 77 | SpecialTokenResponse, 78 | CoreTokenType, 79 | CoreTokenIntrospectionResponse, 80 | CoreRevocableToken, 81 | CoreRevocationErrorResponse, 82 | >; 83 | ``` 84 | with: 85 | ```rust 86 | type SpecialClient< 87 | HasAuthUrl = EndpointNotSet, 88 | HasDeviceAuthUrl = EndpointNotSet, 89 | HasIntrospectionUrl = EndpointNotSet, 90 | HasRevocationUrl = EndpointNotSet, 91 | HasTokenUrl = EndpointNotSet, 92 | HasUserInfoUrl = EndpointNotSet, 93 | > = Client< 94 | EmptyAdditionalClaims, 95 | CoreAuthDisplay, 96 | CoreGenderClaim, 97 | CoreJweContentEncryptionAlgorithm, 98 | CoreJsonWebKey, 99 | CoreAuthPrompt, 100 | StandardErrorResponse, 101 | SpecialTokenResponse, 102 | CoreTokenIntrospectionResponse, 103 | CoreRevocableToken, 104 | CoreRevocationErrorResponse, 105 | HasAuthUrl, 106 | HasDeviceAuthUrl, 107 | HasIntrospectionUrl, 108 | HasRevocationUrl, 109 | HasTokenUrl, 110 | HasUserInfoUrl, 111 | >; 112 | ``` 113 | The default values (`= EndpointNotSet`) are optional but often helpful since they will allow you 114 | to instantiate a client using `SpecialClient::new()` instead of having to specify 115 | `SpecialClient::::new()`. 116 | 117 | Also note that the `CoreJwsSigningAlgorithm` (`JS`), `CoreJsonWebKeyType` (`JT`), 118 | `CoreJsonWebKeyUse` (`JU`), and `CoreTokenType` (`TT`) type parameters have been removed (see 119 | below) since they are now implied by the `JsonWebKey` (`K`) and `TokenResponse` 120 | (`TR`)/`TokenIntrospectionResponse` (`TIR`) type parameters. 121 | 122 | ### Replace JWT-related generic traits with associated types 123 | 124 | Previously, the `JsonWebKey` trait had the following generic type parameters: 125 | ```rust 126 | JS: JwsSigningAlgorithm, 127 | JT: JsonWebKeyType, 128 | JU: JsonWebKeyUse, 129 | ``` 130 | 131 | In the 4.0 release, these generic type parameters have been removed and replaced with two associated 132 | types: 133 | ```rust 134 | /// Allowed key usage. 135 | type KeyUse: JsonWebKeyUse; 136 | 137 | /// JSON Web Signature (JWS) algorithm. 138 | type SigningAlgorithm: JwsSigningAlgorithm; 139 | ``` 140 | 141 | The `JT` type parameter was similarly removed from the `JwsSigningAlgorithm` trait and replaced 142 | with an associated type: 143 | ```rust 144 | /// Key type (e.g., RSA). 145 | type KeyType: JsonWebKeyType; 146 | ``` 147 | 148 | Similar changes were made to the lesser-used `PrivateSigningKey` and `JweContentEncryptionAlgorithm` 149 | traits. 150 | 151 | With the conversion to associated types, many generic type parameters throughout this crate became 152 | redundant and were removed in the 4.0 release. For example, the `Client` no longer needs the 153 | `JS`, `JT`, or `JU` parameters, which are implied by the `JsonWebKey` (`K`) type. 154 | 155 | ### Rename endpoint getters and setters for consistency 156 | 157 | The 2.0 release aimed to align the naming of each endpoint with the terminology used in the relevant 158 | RFC. For example, [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1) uses the 159 | term "endpoint URI" to refer to the authorization and token endpoints, while 160 | [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009#section-2) refers to the 161 | "token revocation endpoint URL," and 162 | [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662#section-2) uses neither "URI" nor "URL" 163 | to describe the introspection endpoint. However, the renaming in 2.0 was both internally 164 | inconsistent, and inconsistent with the specs. 165 | 166 | In 4.0, the `Client`'s getters and setters for each endpoint are now named as follows: 167 | * Authorization endpoint: `auth_uri()`/`set_auth_uri()` (newly added) 168 | * Token endpoint: `token_uri()`/`set_token_uri()` (newly added) 169 | * Redirect: `redirect_uri()`/`set_redirect_uri()` (no change to setter) 170 | * Revocation endpoint: `revocation_url()`/`set_revocation_url()` 171 | * Introspection endpoint: `introspection_url()`/`set_introspection_url()` 172 | * Device authorization endpoint: `device_authorization_url()`/`set_device_authorization_url()` 173 | * User info: `user_info_url()`/`set_user_info_url()` (newly added) 174 | 175 | ### Use stateful HTTP clients 176 | 177 | Previously, the HTTP clients provided by this crate were stateless. For example, the 178 | `openidconnect::reqwest::async_http_client()` method would instantiate a new `reqwest::Client` for 179 | each request. This meant that TCP connections could not be reused across requests, and customizing 180 | HTTP clients (e.g., adding a custom request header to every request) was inconvenient. 181 | 182 | The 4.0 release introduces two new traits: `AsyncHttpClient` and `SyncHttpClient`. Each 183 | `request_async()` and `request()` method now accepts a reference to a type that implements these 184 | traits, respectively, rather than a function type. 185 | 186 | > [!WARNING] 187 | > To prevent 188 | [SSRF](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html) 189 | vulnerabilities, be sure to configure the HTTP client **not to follow redirects**. For example, use 190 | > [`redirect::Policy::none`](https://docs.rs/reqwest/latest/reqwest/redirect/struct.Policy.html#method.none) 191 | > when using `reqwest`, or 192 | > [`redirects(0)`](https://docs.rs/ureq/latest/ureq/struct.AgentBuilder.html#method.redirects) 193 | > when using `ureq`. 194 | 195 | The `AsyncHttpClient` trait is implemented for the following types: 196 | * `reqwest::Client` (when the default `reqwest` feature is enabled) 197 | * Any function type that implements: 198 | ```rust 199 | Fn(HttpRequest) -> F 200 | where 201 | E: std::error::Error + 'static, 202 | F: Future>, 203 | ``` 204 | To implement a custom asynchronous HTTP client, either directly implement the `AsyncHttpClient` 205 | trait, or use a function that implements the signature above. 206 | 207 | The `SyncHttpClient` trait is implemented for the following types: 208 | * `reqwest::blocking::Client` (when the `reqwest-blocking` feature is enabled; see below) 209 | * `ureq::Agent` (when the `ureq` feature is enabled) 210 | * `openidconnect::CurlHttpClient` (when the `curl` feature is enabled) 211 | * Any function type that implements: 212 | ```rust 213 | Fn(HttpRequest) -> Result 214 | where 215 | E: std::error::Error + 'static, 216 | ``` 217 | To implement a custom synchronous HTTP client, either directly implement the `SyncHttpClient` 218 | trait, or use a function that implements the signature above. 219 | 220 | ### Upgrade `http` to 1.0 and `reqwest` to 0.12 221 | 222 | The 4.0 release of this crate depends on the new stable [`http`](https://docs.rs/http/latest/http/) 223 | 1.0 release, which affects various public interfaces. In particular, `reqwest` has been upgraded 224 | to 0.12, which uses `http` 1.0. 225 | 226 | ### Enable the `reqwest-blocking` feature to use the synchronous `reqwest` HTTP client 227 | 228 | In 4.0, enabling the (default) `reqwest` feature also enabled `reqwest`'s `blocking` feature. 229 | To reduce dependencies and improve compilation speed, the `reqwest` feature now only enables 230 | `reqwest`'s asynchronous (non-blocking) client. To use the synchronous (blocking) client, enable the 231 | `reqwest-blocking` feature in `Cargo.toml`: 232 | ```toml 233 | openidconnect = { version = "4", features = ["reqwest-blocking" ] } 234 | ``` 235 | 236 | ### Use `http::{Request, Response}` for custom HTTP clients 237 | 238 | The `HttpRequest` and `HttpResponse` structs have been replaced with type aliases to 239 | [`http::Request`](https://docs.rs/http/latest/http/request/struct.Request.html) and 240 | [`http::Response`](https://docs.rs/http/latest/http/response/struct.Response.html), respectively. 241 | Custom HTTP clients will need to be updated to use the `http` types. See the 242 | [`reqwest` client implementations](https://github.com/ramosbugs/oauth2-rs/blob/23b952b23e6069525bc7e4c4f2c4924b8d28ce3a/src/reqwest.rs) 243 | in the underlying `oauth2` crate for an example. 244 | 245 | ### Replace `TT` generic type parameter in `OAuth2TokenResponse` with associated type 246 | 247 | Previously, the `TokenResponse`, `OAuth2TokenResponse`, and `TokenIntrospectionResponse` traits had 248 | a generic type parameter `TT: TokenType`. This has been replaced with an associated type called 249 | `TokenType` in `OAuth2TokenResponse` and `TokenIntrospectionResponse`. 250 | Uses of `CoreTokenResponse` and `CoreTokenIntrospectionResponse` should continue to work without 251 | changes, but custom implementations of either trait will need to be updated to replace the type 252 | parameter with an associated type. 253 | 254 | #### Remove `TT` generic type parameter from `Client` and each `*Request` type 255 | 256 | Removing the `TT` generic type parameter from `TokenResponse` (see above) made the `TT` parameters 257 | to `Client` and each `*Request` (e.g., `CodeTokenRequest`) redundant. Consequently, the `TT` 258 | parameter has been removed from each of these types. `CoreClient` should continue to work 259 | without any changes, but code that provides generic types for `Client` or any of the `*Response` 260 | types will need to be updated to remove the `TT` type parameter. 261 | 262 | ### Add `Display` to `ErrorResponse` trait 263 | 264 | To improve error messages, the 265 | [`RequestTokenError::ServerResponse`](https://docs.rs/oauth2/latest/oauth2/enum.RequestTokenError.html#variant.ServerResponse) 266 | enum variant now prints a message describing the server response using the `Display` trait. For most 267 | users (i.e., those using the default 268 | [`StandardErrorResponse`](https://docs.rs/oauth2/latest/oauth2/struct.StandardErrorResponse.html)), 269 | this does not require any code changes. However, users providing their own implementations 270 | of the `ErrorResponse` trait must now implement the `Display` trait. See 271 | `oauth2::StandardErrorResponse`'s 272 | [`Display` implementation](https://github.com/ramosbugs/oauth2-rs/blob/9d8f11addf819134f15c6d7f03276adb3d32e80b/src/error.rs#L88-L108) 273 | for an example. 274 | 275 | ### Remove the `jwk-alg` feature flag 276 | 277 | The 4.0 release removes the `jwk-alg` feature flag and unconditionally deserializes the optional 278 | `alg` field in `CoreJsonWebKey`. If a key specifies the `alg` field, the key may only be used for 279 | the purposes of verifying signatures using that specific JWS signature algorithm. By comparison, 280 | the 3.0 release ignored the `alg` field unless the `jwk-alg` feature flag was enabled. 281 | 282 | ### Enable the `timing-resistant-secret-traits` feature flag to securely compare secrets 283 | 284 | OpenID Connect flows require comparing secrets (e.g., `CsrfToken` and `Nonce`) received from 285 | providers. To do so securely 286 | while avoiding [timing side-channels](https://en.wikipedia.org/wiki/Timing_attack), the 287 | comparison must be done in constant time, either using a constant-time crate such as 288 | [`constant_time_eq`](https://crates.io/crates/constant_time_eq) (which could break if a future 289 | compiler version decides to be overly smart 290 | about its optimizations), or by first computing a cryptographically-secure hash (e.g., SHA-256) 291 | of both values and then comparing the hashes using `==`. 292 | 293 | The `timing-resistant-secret-traits` feature flag adds a safe (but comparatively expensive) 294 | `PartialEq` implementation to the secret types. Timing side-channels are why `PartialEq` is 295 | not auto-derived for this crate's secret types, and the lack of `PartialEq` is intended to 296 | prompt users to think more carefully about these comparisons. 297 | 298 | In the 3.0 release, the `Nonce` type implemented `PartialEq` by default, which also allowed the 299 | `IdToken`, `IdTokenClaims`, and `IdTokenFields` types to implement `PartialEq`. In 4.0, these 300 | types implement `PartialEq` only if the `timing-resistant-secret-traits` feature flag is enabled. 301 | 302 | ### Move `hash_bytes()` method from `JwsSignatureAlgorithm` trait to `JsonWebKey` 303 | 304 | Certain JWS signature algorithms (e.g., `EdDSA`) require information from the corresponding public 305 | key (e.g., the `crv` value) to determine which hash function to use for computing the `at_hash` and 306 | `c_hash` ID token claims. To accommodate this requirement, the 4.0 release moves the `hash_bytes()` 307 | method from the `JwsSignatureAlgorithm` trait to the `JsonWebKey` trait. 308 | 309 | The `AccessTokenHash::from_token()` and `AuthorizationCodeHash::from_code()` methods now require 310 | a `JsonWebKey` as an argument. 311 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::types::localized::join_language_tag_key; 2 | use crate::{LanguageTag, LocalizedClaim}; 3 | 4 | use chrono::{DateTime, TimeZone, Utc}; 5 | use serde::de::value::MapDeserializer; 6 | use serde::de::{DeserializeOwned, Deserializer, Error, MapAccess, Visitor}; 7 | use serde::{Deserialize, Serialize, Serializer}; 8 | use serde_json::from_value; 9 | use serde_value::ValueDeserializer; 10 | use serde_with::{DeserializeAs, SerializeAs}; 11 | 12 | use std::cmp::PartialEq; 13 | use std::fmt::{Debug, Display, Formatter, Result as FormatterResult}; 14 | use std::marker::PhantomData; 15 | 16 | pub(crate) fn deserialize_string_or_vec<'de, T, D>(deserializer: D) -> Result, D::Error> 17 | where 18 | T: DeserializeOwned, 19 | D: Deserializer<'de>, 20 | { 21 | let value: serde_json::Value = Deserialize::deserialize(deserializer)?; 22 | match from_value::>(value.clone()) { 23 | Ok(val) => Ok(val), 24 | Err(_) => { 25 | let single_val: T = from_value(value).map_err(Error::custom)?; 26 | Ok(vec![single_val]) 27 | } 28 | } 29 | } 30 | 31 | pub(crate) fn deserialize_string_or_vec_opt<'de, T, D>( 32 | deserializer: D, 33 | ) -> Result>, D::Error> 34 | where 35 | T: DeserializeOwned, 36 | D: Deserializer<'de>, 37 | { 38 | let value: serde_json::Value = Deserialize::deserialize(deserializer)?; 39 | match from_value::>>(value.clone()) { 40 | Ok(val) => Ok(val), 41 | Err(_) => { 42 | let single_val: T = from_value(value).map_err(Error::custom)?; 43 | Ok(Some(vec![single_val])) 44 | } 45 | } 46 | } 47 | 48 | // Attempt to deserialize the value; if the value is null or an error occurs, return None. 49 | // This is useful when deserializing fields that may mean different things in different 50 | // contexts, and where we would rather ignore the result than fail to deserialize. For example, 51 | // the fields in JWKs are not well defined; extensions could theoretically define their own 52 | // field names that overload field names used by other JWK types. 53 | pub(crate) fn deserialize_option_or_none<'de, T, D>(deserializer: D) -> Result, D::Error> 54 | where 55 | T: DeserializeOwned, 56 | D: Deserializer<'de>, 57 | { 58 | let value: serde_json::Value = Deserialize::deserialize(deserializer)?; 59 | match from_value::>(value) { 60 | Ok(val) => Ok(val), 61 | Err(_) => Ok(None), 62 | } 63 | } 64 | 65 | pub trait DeserializeMapField: Sized { 66 | fn deserialize_map_field<'de, V>( 67 | map: &mut V, 68 | field_name: &'static str, 69 | language_tag: Option, 70 | field_value: Option, 71 | ) -> Result 72 | where 73 | V: MapAccess<'de>; 74 | } 75 | 76 | impl DeserializeMapField for T 77 | where 78 | T: DeserializeOwned, 79 | { 80 | fn deserialize_map_field<'de, V>( 81 | map: &mut V, 82 | field_name: &'static str, 83 | language_tag: Option, 84 | field_value: Option, 85 | ) -> Result 86 | where 87 | V: MapAccess<'de>, 88 | { 89 | if field_value.is_some() { 90 | return Err(serde::de::Error::duplicate_field(field_name)); 91 | } else if let Some(language_tag) = language_tag { 92 | return Err(serde::de::Error::custom(format!( 93 | "unexpected language tag `{language_tag}` for key `{field_name}`" 94 | ))); 95 | } 96 | map.next_value().map_err(|err| { 97 | V::Error::custom(format!( 98 | "{}: {err}", 99 | join_language_tag_key(field_name, language_tag.as_ref()) 100 | )) 101 | }) 102 | } 103 | } 104 | 105 | impl DeserializeMapField for LocalizedClaim 106 | where 107 | T: DeserializeOwned, 108 | { 109 | fn deserialize_map_field<'de, V>( 110 | map: &mut V, 111 | field_name: &'static str, 112 | language_tag: Option, 113 | field_value: Option, 114 | ) -> Result 115 | where 116 | V: MapAccess<'de>, 117 | { 118 | let mut localized_claim = field_value.unwrap_or_default(); 119 | if localized_claim.contains_key(language_tag.as_ref()) { 120 | return Err(serde::de::Error::custom(format!( 121 | "duplicate field `{}`", 122 | join_language_tag_key(field_name, language_tag.as_ref()) 123 | ))); 124 | } 125 | 126 | let localized_value = map.next_value().map_err(|err| { 127 | V::Error::custom(format!( 128 | "{}: {err}", 129 | join_language_tag_key(field_name, language_tag.as_ref()) 130 | )) 131 | })?; 132 | localized_claim.insert(language_tag, localized_value); 133 | 134 | Ok(localized_claim) 135 | } 136 | } 137 | 138 | // Some providers return boolean values as strings. Provide support for 139 | // parsing using stdlib. 140 | #[cfg(feature = "accept-string-booleans")] 141 | pub(crate) mod serde_string_bool { 142 | use serde::{de, Deserializer}; 143 | 144 | use std::fmt; 145 | 146 | pub fn deserialize<'de, D>(deserializer: D) -> Result 147 | where 148 | D: Deserializer<'de>, 149 | { 150 | struct BooleanLikeVisitor; 151 | 152 | impl<'de> de::Visitor<'de> for BooleanLikeVisitor { 153 | type Value = bool; 154 | 155 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 156 | formatter.write_str("A boolean-like value") 157 | } 158 | 159 | fn visit_bool(self, v: bool) -> Result 160 | where 161 | E: de::Error, 162 | { 163 | Ok(v) 164 | } 165 | 166 | fn visit_str(self, v: &str) -> Result 167 | where 168 | E: de::Error, 169 | { 170 | v.parse().map_err(E::custom) 171 | } 172 | } 173 | deserializer.deserialize_any(BooleanLikeVisitor) 174 | } 175 | } 176 | 177 | /// Serde space-delimited string serializer for an `Option>`. 178 | /// 179 | /// This function serializes a string vector into a single space-delimited string. 180 | /// If `string_vec_opt` is `None`, the function serializes it as `None` (e.g., `null` 181 | /// in the case of JSON serialization). 182 | pub(crate) fn serialize_space_delimited_vec( 183 | vec: &[T], 184 | serializer: S, 185 | ) -> Result 186 | where 187 | T: AsRef, 188 | S: Serializer, 189 | { 190 | let space_delimited = vec 191 | .iter() 192 | .map(AsRef::::as_ref) 193 | .collect::>() 194 | .join(" "); 195 | 196 | serializer.serialize_str(&space_delimited) 197 | } 198 | 199 | pub(crate) trait FlattenFilter { 200 | fn should_include(field_name: &str) -> bool; 201 | } 202 | 203 | /// Helper container for filtering map keys out of serde(flatten). This is needed because 204 | /// [`crate::StandardClaims`] doesn't have a fixed set of field names due to its support for 205 | /// localized claims. Consequently, serde by default passes all of the claims to the deserializer 206 | /// for `AC` (additional claims), leading to duplicate claims. [`FilteredFlatten`] is used for 207 | /// eliminating the duplicate claims. 208 | #[derive(Serialize)] 209 | pub(crate) struct FilteredFlatten 210 | where 211 | F: FlattenFilter, 212 | T: DeserializeOwned + Serialize, 213 | { 214 | // We include another level of flattening here because the derived flatten 215 | // ([`serde::private::de::FlatMapDeserializer`]) seems to support a wider set of types 216 | // (e.g., various forms of enum tagging) than [`serde_value::ValueDeserializer`]. 217 | #[serde(flatten)] 218 | inner: Flatten, 219 | #[serde(skip)] 220 | _phantom: PhantomData, 221 | } 222 | impl From for FilteredFlatten 223 | where 224 | F: FlattenFilter, 225 | T: DeserializeOwned + Serialize, 226 | { 227 | fn from(value: T) -> Self { 228 | Self { 229 | inner: Flatten { inner: value }, 230 | _phantom: PhantomData, 231 | } 232 | } 233 | } 234 | impl AsRef for FilteredFlatten 235 | where 236 | F: FlattenFilter, 237 | T: DeserializeOwned + Serialize, 238 | { 239 | fn as_ref(&self) -> &T { 240 | self.inner.as_ref() 241 | } 242 | } 243 | impl AsMut for FilteredFlatten 244 | where 245 | F: FlattenFilter, 246 | T: DeserializeOwned + Serialize, 247 | { 248 | fn as_mut(&mut self) -> &mut T { 249 | self.inner.as_mut() 250 | } 251 | } 252 | impl PartialEq for FilteredFlatten 253 | where 254 | F: FlattenFilter, 255 | T: DeserializeOwned + PartialEq + Serialize, 256 | { 257 | fn eq(&self, other: &Self) -> bool { 258 | self.inner == other.inner 259 | } 260 | } 261 | impl Clone for FilteredFlatten 262 | where 263 | F: FlattenFilter, 264 | T: Clone + DeserializeOwned + Serialize, 265 | { 266 | fn clone(&self) -> Self { 267 | Self { 268 | inner: Flatten { 269 | inner: self.inner.inner.clone(), 270 | }, 271 | _phantom: PhantomData, 272 | } 273 | } 274 | } 275 | impl Debug for FilteredFlatten 276 | where 277 | F: FlattenFilter, 278 | T: Debug + DeserializeOwned + Serialize, 279 | { 280 | // Transparent Debug since we don't care about this struct. 281 | fn fmt(&self, f: &mut Formatter) -> FormatterResult { 282 | Debug::fmt(&self.inner, f) 283 | } 284 | } 285 | 286 | impl<'de, F, T> Deserialize<'de> for FilteredFlatten 287 | where 288 | F: FlattenFilter, 289 | T: DeserializeOwned + Serialize, 290 | { 291 | fn deserialize(deserializer: D) -> Result 292 | where 293 | D: Deserializer<'de>, 294 | { 295 | struct MapVisitor(PhantomData<(F, T)>); 296 | 297 | impl<'de, F, T> Visitor<'de> for MapVisitor 298 | where 299 | F: FlattenFilter, 300 | T: DeserializeOwned + Serialize, 301 | { 302 | type Value = Flatten; 303 | 304 | fn expecting(&self, formatter: &mut Formatter) -> FormatterResult { 305 | formatter.write_str("map type T") 306 | } 307 | 308 | fn visit_map(self, mut map: V) -> Result 309 | where 310 | V: MapAccess<'de>, 311 | { 312 | let mut entries = Vec::<(serde_value::Value, serde_value::Value)>::new(); 313 | // JSON only supports String keys, and we really only need to support JSON input. 314 | while let Some(key) = map.next_key::()? { 315 | let key_str = String::deserialize(ValueDeserializer::new(key.clone()))?; 316 | if F::should_include(&key_str) { 317 | entries.push((key, map.next_value()?)); 318 | } 319 | } 320 | 321 | Deserialize::deserialize(MapDeserializer::new(entries.into_iter())) 322 | .map_err(serde_value::DeserializerError::into_error) 323 | } 324 | } 325 | 326 | Ok(FilteredFlatten { 327 | inner: deserializer.deserialize_map(MapVisitor(PhantomData::<(F, T)>))?, 328 | _phantom: PhantomData, 329 | }) 330 | } 331 | } 332 | 333 | #[derive(Deserialize, Serialize)] 334 | struct Flatten 335 | where 336 | T: DeserializeOwned + Serialize, 337 | { 338 | #[serde(flatten, bound = "T: DeserializeOwned + Serialize")] 339 | inner: T, 340 | } 341 | impl AsRef for Flatten 342 | where 343 | T: DeserializeOwned + Serialize, 344 | { 345 | fn as_ref(&self) -> &T { 346 | &self.inner 347 | } 348 | } 349 | impl AsMut for Flatten 350 | where 351 | T: DeserializeOwned + Serialize, 352 | { 353 | fn as_mut(&mut self) -> &mut T { 354 | &mut self.inner 355 | } 356 | } 357 | impl PartialEq for Flatten 358 | where 359 | T: DeserializeOwned + PartialEq + Serialize, 360 | { 361 | fn eq(&self, other: &Self) -> bool { 362 | self.inner == other.inner 363 | } 364 | } 365 | impl Debug for Flatten 366 | where 367 | T: Debug + DeserializeOwned + Serialize, 368 | { 369 | // Transparent Debug since we don't care about this struct. 370 | fn fmt(&self, f: &mut Formatter) -> FormatterResult { 371 | Debug::fmt(&self.inner, f) 372 | } 373 | } 374 | 375 | pub(crate) fn join_vec(entries: &[T]) -> String 376 | where 377 | T: AsRef, 378 | { 379 | entries 380 | .iter() 381 | .map(AsRef::as_ref) 382 | .collect::>() 383 | .join(" ") 384 | } 385 | 386 | /// Newtype around a bool, optionally supporting string values. 387 | #[derive(Debug, Deserialize, Serialize)] 388 | #[serde(transparent)] 389 | pub(crate) struct Boolean( 390 | #[cfg_attr( 391 | feature = "accept-string-booleans", 392 | serde(deserialize_with = "crate::helpers::serde_string_bool::deserialize") 393 | )] 394 | pub bool, 395 | ); 396 | impl Boolean { 397 | pub(crate) fn into_inner(self) -> bool { 398 | self.0 399 | } 400 | } 401 | impl Display for Boolean { 402 | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { 403 | Display::fmt(&self.0, f) 404 | } 405 | } 406 | 407 | /// Timestamp as seconds since the unix epoch, or optionally an ISO 8601 string. 408 | #[derive(Debug, Deserialize, Serialize)] 409 | #[serde(untagged)] 410 | pub(crate) enum Timestamp { 411 | Seconds(serde_json::Number), 412 | #[cfg(feature = "accept-rfc3339-timestamps")] 413 | Rfc3339(String), 414 | } 415 | impl Timestamp { 416 | // The spec is ambiguous about whether seconds should be expressed as integers, or 417 | // whether floating-point values are allowed. For compatibility with a wide range of 418 | // clients, we round down to the nearest second. 419 | pub(crate) fn from_utc(utc: &DateTime) -> Self { 420 | Timestamp::Seconds(utc.timestamp().into()) 421 | } 422 | 423 | pub(crate) fn to_utc(&self) -> Result, ()> { 424 | match self { 425 | Timestamp::Seconds(seconds) => { 426 | let (secs, nsecs) = if seconds.is_i64() { 427 | (seconds.as_i64().ok_or(())?, 0u32) 428 | } else { 429 | let secs_f64 = seconds.as_f64().ok_or(())?; 430 | let secs = secs_f64.floor(); 431 | ( 432 | secs as i64, 433 | ((secs_f64 - secs) * 1_000_000_000.).floor() as u32, 434 | ) 435 | }; 436 | Utc.timestamp_opt(secs, nsecs).single().ok_or(()) 437 | } 438 | #[cfg(feature = "accept-rfc3339-timestamps")] 439 | Timestamp::Rfc3339(iso) => { 440 | let datetime = DateTime::parse_from_rfc3339(iso).map_err(|_| ())?; 441 | Ok(datetime.into()) 442 | } 443 | } 444 | } 445 | } 446 | 447 | impl Display for Timestamp { 448 | fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { 449 | match self { 450 | Timestamp::Seconds(seconds) => Display::fmt(seconds, f), 451 | #[cfg(feature = "accept-rfc3339-timestamps")] 452 | Timestamp::Rfc3339(iso) => Display::fmt(iso, f), 453 | } 454 | } 455 | } 456 | 457 | impl<'de> DeserializeAs<'de, DateTime> for Timestamp { 458 | fn deserialize_as(deserializer: D) -> Result, D::Error> 459 | where 460 | D: Deserializer<'de>, 461 | { 462 | let seconds: Timestamp = Deserialize::deserialize(deserializer)?; 463 | seconds.to_utc().map_err(|_| { 464 | serde::de::Error::custom(format!( 465 | "failed to parse `{}` as UTC datetime (in seconds)", 466 | seconds 467 | )) 468 | }) 469 | } 470 | } 471 | 472 | impl SerializeAs> for Timestamp { 473 | fn serialize_as(source: &DateTime, serializer: S) -> Result 474 | where 475 | S: Serializer, 476 | { 477 | Timestamp::from_utc(source).serialize(serializer) 478 | } 479 | } 480 | 481 | new_type![ 482 | #[derive(Deserialize, Hash, Serialize)] 483 | pub(crate) Base64UrlEncodedBytes( 484 | #[serde(with = "serde_base64url_byte_array")] 485 | Vec 486 | ) 487 | ]; 488 | 489 | mod serde_base64url_byte_array { 490 | use crate::core::base64_url_safe_no_pad; 491 | 492 | use base64::prelude::BASE64_URL_SAFE_NO_PAD; 493 | use base64::Engine; 494 | use serde::de::Error; 495 | use serde::{Deserialize, Deserializer, Serializer}; 496 | use serde_json::{from_value, Value}; 497 | 498 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 499 | where 500 | D: Deserializer<'de>, 501 | { 502 | let value: Value = Deserialize::deserialize(deserializer)?; 503 | let base64_encoded: String = from_value(value).map_err(D::Error::custom)?; 504 | 505 | base64_url_safe_no_pad() 506 | .decode(&base64_encoded) 507 | .map_err(|err| { 508 | D::Error::custom(format!( 509 | "invalid base64url encoding `{}`: {:?}", 510 | base64_encoded, err 511 | )) 512 | }) 513 | } 514 | 515 | pub fn serialize(v: &[u8], serializer: S) -> Result 516 | where 517 | S: Serializer, 518 | { 519 | let base64_encoded = BASE64_URL_SAFE_NO_PAD.encode(v); 520 | serializer.serialize_str(&base64_encoded) 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /src/discovery/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::http_utils::{check_content_type, MIME_TYPE_JSON}; 2 | use crate::{ 3 | AsyncHttpClient, AuthDisplay, AuthUrl, AuthenticationContextClass, ClaimName, ClaimType, 4 | ClientAuthMethod, GrantType, HttpRequest, HttpResponse, IssuerUrl, JsonWebKey, JsonWebKeySet, 5 | JsonWebKeySetUrl, JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, 6 | JwsSigningAlgorithm, LanguageTag, OpPolicyUrl, OpTosUrl, RegistrationUrl, ResponseMode, 7 | ResponseType, ResponseTypes, Scope, ServiceDocUrl, SubjectIdentifierType, SyncHttpClient, 8 | TokenUrl, UserInfoUrl, 9 | }; 10 | 11 | use http::header::{HeaderValue, ACCEPT}; 12 | use http::method::Method; 13 | use http::status::StatusCode; 14 | use serde::de::DeserializeOwned; 15 | use serde::{Deserialize, Serialize}; 16 | use serde_with::{serde_as, skip_serializing_none, VecSkipError}; 17 | use thiserror::Error; 18 | 19 | use std::fmt::Debug; 20 | use std::future::Future; 21 | 22 | #[cfg(test)] 23 | mod tests; 24 | 25 | const CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration"; 26 | 27 | /// Trait for adding extra fields to [`ProviderMetadata`]. 28 | pub trait AdditionalProviderMetadata: Clone + Debug + DeserializeOwned + Serialize {} 29 | 30 | // In order to support serde flatten, this must be an empty struct rather than an empty 31 | // tuple struct. 32 | /// Empty (default) extra [`ProviderMetadata`] fields. 33 | #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] 34 | pub struct EmptyAdditionalProviderMetadata {} 35 | impl AdditionalProviderMetadata for EmptyAdditionalProviderMetadata {} 36 | 37 | /// Provider metadata returned by [OpenID Connect Discovery]( 38 | /// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). 39 | #[serde_as] 40 | #[skip_serializing_none] 41 | #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] 42 | #[allow(clippy::type_complexity)] 43 | pub struct ProviderMetadata 44 | where 45 | A: AdditionalProviderMetadata, 46 | AD: AuthDisplay, 47 | CA: ClientAuthMethod, 48 | CN: ClaimName, 49 | CT: ClaimType, 50 | G: GrantType, 51 | JE: JweContentEncryptionAlgorithm< 52 | KeyType = ::KeyType, 53 | >, 54 | JK: JweKeyManagementAlgorithm, 55 | K: JsonWebKey, 56 | RM: ResponseMode, 57 | RT: ResponseType, 58 | S: SubjectIdentifierType, 59 | { 60 | issuer: IssuerUrl, 61 | authorization_endpoint: AuthUrl, 62 | token_endpoint: Option, 63 | userinfo_endpoint: Option, 64 | jwks_uri: JsonWebKeySetUrl, 65 | #[serde(default = "JsonWebKeySet::default", skip)] 66 | jwks: JsonWebKeySet, 67 | registration_endpoint: Option, 68 | scopes_supported: Option>, 69 | #[serde(bound(deserialize = "RT: ResponseType"))] 70 | response_types_supported: Vec>, 71 | #[serde(bound(deserialize = "RM: ResponseMode"))] 72 | response_modes_supported: Option>, 73 | #[serde(bound(deserialize = "G: GrantType"))] 74 | grant_types_supported: Option>, 75 | acr_values_supported: Option>, 76 | #[serde(bound(deserialize = "S: SubjectIdentifierType"))] 77 | subject_types_supported: Vec, 78 | #[serde(bound(deserialize = "K: JsonWebKey"))] 79 | #[serde_as(as = "VecSkipError<_>")] 80 | id_token_signing_alg_values_supported: Vec, 81 | #[serde( 82 | bound(deserialize = "JK: JweKeyManagementAlgorithm"), 83 | default = "Option::default" 84 | )] 85 | #[serde_as(as = "Option>")] 86 | id_token_encryption_alg_values_supported: Option>, 87 | #[serde( 88 | bound( 89 | deserialize = "JE: JweContentEncryptionAlgorithm::KeyType>" 90 | ), 91 | default = "Option::default" 92 | )] 93 | #[serde_as(as = "Option>")] 94 | id_token_encryption_enc_values_supported: Option>, 95 | #[serde(bound(deserialize = "K: JsonWebKey"), default = "Option::default")] 96 | #[serde_as(as = "Option>")] 97 | userinfo_signing_alg_values_supported: Option>, 98 | #[serde( 99 | bound(deserialize = "JK: JweKeyManagementAlgorithm"), 100 | default = "Option::default" 101 | )] 102 | #[serde_as(as = "Option>")] 103 | userinfo_encryption_alg_values_supported: Option>, 104 | #[serde( 105 | bound( 106 | deserialize = "JE: JweContentEncryptionAlgorithm::KeyType>" 107 | ), 108 | default = "Option::default" 109 | )] 110 | #[serde_as(as = "Option>")] 111 | userinfo_encryption_enc_values_supported: Option>, 112 | #[serde(bound(deserialize = "K: JsonWebKey"), default = "Option::default")] 113 | #[serde_as(as = "Option>")] 114 | request_object_signing_alg_values_supported: Option>, 115 | #[serde( 116 | bound(deserialize = "JK: JweKeyManagementAlgorithm"), 117 | default = "Option::default" 118 | )] 119 | #[serde_as(as = "Option>")] 120 | request_object_encryption_alg_values_supported: Option>, 121 | #[serde( 122 | bound( 123 | deserialize = "JE: JweContentEncryptionAlgorithm::KeyType>" 124 | ), 125 | default = "Option::default" 126 | )] 127 | #[serde_as(as = "Option>")] 128 | request_object_encryption_enc_values_supported: Option>, 129 | #[serde(bound(deserialize = "CA: ClientAuthMethod"))] 130 | token_endpoint_auth_methods_supported: Option>, 131 | #[serde(bound(deserialize = "K: JsonWebKey"), default = "Option::default")] 132 | #[serde_as(as = "Option>")] 133 | token_endpoint_auth_signing_alg_values_supported: Option>, 134 | #[serde(bound(deserialize = "AD: AuthDisplay"))] 135 | display_values_supported: Option>, 136 | #[serde(bound(deserialize = "CT: ClaimType"))] 137 | claim_types_supported: Option>, 138 | #[serde(bound(deserialize = "CN: ClaimName"))] 139 | claims_supported: Option>, 140 | service_documentation: Option, 141 | claims_locales_supported: Option>, 142 | ui_locales_supported: Option>, 143 | claims_parameter_supported: Option, 144 | request_parameter_supported: Option, 145 | request_uri_parameter_supported: Option, 146 | require_request_uri_registration: Option, 147 | op_policy_uri: Option, 148 | op_tos_uri: Option, 149 | 150 | #[serde(bound(deserialize = "A: AdditionalProviderMetadata"), flatten)] 151 | additional_metadata: A, 152 | } 153 | impl 154 | ProviderMetadata 155 | where 156 | A: AdditionalProviderMetadata, 157 | AD: AuthDisplay, 158 | CA: ClientAuthMethod, 159 | CN: ClaimName, 160 | CT: ClaimType, 161 | G: GrantType, 162 | JE: JweContentEncryptionAlgorithm< 163 | KeyType = ::KeyType, 164 | >, 165 | JK: JweKeyManagementAlgorithm, 166 | K: JsonWebKey, 167 | RM: ResponseMode, 168 | RT: ResponseType, 169 | S: SubjectIdentifierType, 170 | { 171 | /// Instantiates new provider metadata. 172 | pub fn new( 173 | issuer: IssuerUrl, 174 | authorization_endpoint: AuthUrl, 175 | jwks_uri: JsonWebKeySetUrl, 176 | response_types_supported: Vec>, 177 | subject_types_supported: Vec, 178 | id_token_signing_alg_values_supported: Vec, 179 | additional_metadata: A, 180 | ) -> Self { 181 | Self { 182 | issuer, 183 | authorization_endpoint, 184 | token_endpoint: None, 185 | userinfo_endpoint: None, 186 | jwks_uri, 187 | jwks: JsonWebKeySet::new(Vec::new()), 188 | registration_endpoint: None, 189 | scopes_supported: None, 190 | response_types_supported, 191 | response_modes_supported: None, 192 | grant_types_supported: None, 193 | acr_values_supported: None, 194 | subject_types_supported, 195 | id_token_signing_alg_values_supported, 196 | id_token_encryption_alg_values_supported: None, 197 | id_token_encryption_enc_values_supported: None, 198 | userinfo_signing_alg_values_supported: None, 199 | userinfo_encryption_alg_values_supported: None, 200 | userinfo_encryption_enc_values_supported: None, 201 | request_object_signing_alg_values_supported: None, 202 | request_object_encryption_alg_values_supported: None, 203 | request_object_encryption_enc_values_supported: None, 204 | token_endpoint_auth_methods_supported: None, 205 | token_endpoint_auth_signing_alg_values_supported: None, 206 | display_values_supported: None, 207 | claim_types_supported: None, 208 | claims_supported: None, 209 | service_documentation: None, 210 | claims_locales_supported: None, 211 | ui_locales_supported: None, 212 | claims_parameter_supported: None, 213 | request_parameter_supported: None, 214 | request_uri_parameter_supported: None, 215 | require_request_uri_registration: None, 216 | op_policy_uri: None, 217 | op_tos_uri: None, 218 | additional_metadata, 219 | } 220 | } 221 | 222 | field_getters_setters![ 223 | pub self [self] ["provider metadata value"] { 224 | set_issuer -> issuer[IssuerUrl], 225 | set_authorization_endpoint -> authorization_endpoint[AuthUrl], 226 | set_token_endpoint -> token_endpoint[Option], 227 | set_userinfo_endpoint -> userinfo_endpoint[Option], 228 | set_jwks_uri -> jwks_uri[JsonWebKeySetUrl], 229 | set_jwks -> jwks[JsonWebKeySet], 230 | set_registration_endpoint -> registration_endpoint[Option], 231 | set_scopes_supported -> scopes_supported[Option>], 232 | set_response_types_supported -> response_types_supported[Vec>], 233 | set_response_modes_supported -> response_modes_supported[Option>], 234 | set_grant_types_supported -> grant_types_supported[Option>], 235 | set_acr_values_supported 236 | -> acr_values_supported[Option>], 237 | set_subject_types_supported -> subject_types_supported[Vec], 238 | set_id_token_signing_alg_values_supported 239 | -> id_token_signing_alg_values_supported[Vec], 240 | set_id_token_encryption_alg_values_supported 241 | -> id_token_encryption_alg_values_supported[Option>], 242 | set_id_token_encryption_enc_values_supported 243 | -> id_token_encryption_enc_values_supported[Option>], 244 | set_userinfo_signing_alg_values_supported 245 | -> userinfo_signing_alg_values_supported[Option>], 246 | set_userinfo_encryption_alg_values_supported 247 | -> userinfo_encryption_alg_values_supported[Option>], 248 | set_userinfo_encryption_enc_values_supported 249 | -> userinfo_encryption_enc_values_supported[Option>], 250 | set_request_object_signing_alg_values_supported 251 | -> request_object_signing_alg_values_supported[Option>], 252 | set_request_object_encryption_alg_values_supported 253 | -> request_object_encryption_alg_values_supported[Option>], 254 | set_request_object_encryption_enc_values_supported 255 | -> request_object_encryption_enc_values_supported[Option>], 256 | set_token_endpoint_auth_methods_supported 257 | -> token_endpoint_auth_methods_supported[Option>], 258 | set_token_endpoint_auth_signing_alg_values_supported 259 | -> token_endpoint_auth_signing_alg_values_supported[Option>], 260 | set_display_values_supported -> display_values_supported[Option>], 261 | set_claim_types_supported -> claim_types_supported[Option>], 262 | set_claims_supported -> claims_supported[Option>], 263 | set_service_documentation -> service_documentation[Option], 264 | set_claims_locales_supported -> claims_locales_supported[Option>], 265 | set_ui_locales_supported -> ui_locales_supported[Option>], 266 | set_claims_parameter_supported -> claims_parameter_supported[Option], 267 | set_request_parameter_supported -> request_parameter_supported[Option], 268 | set_request_uri_parameter_supported -> request_uri_parameter_supported[Option], 269 | set_require_request_uri_registration -> require_request_uri_registration[Option], 270 | set_op_policy_uri -> op_policy_uri[Option], 271 | set_op_tos_uri -> op_tos_uri[Option], 272 | } 273 | ]; 274 | 275 | /// Fetches the OpenID Connect Discovery document and associated JSON Web Key Set from the 276 | /// OpenID Connect Provider. 277 | pub fn discover( 278 | issuer_url: &IssuerUrl, 279 | http_client: &C, 280 | ) -> Result::Error>> 281 | where 282 | C: SyncHttpClient, 283 | { 284 | let discovery_url = issuer_url 285 | .join(CONFIG_URL_SUFFIX) 286 | .map_err(DiscoveryError::UrlParse)?; 287 | 288 | http_client 289 | .call( 290 | Self::discovery_request(discovery_url.clone()).map_err(|err| { 291 | DiscoveryError::Other(format!("failed to prepare request: {err}")) 292 | })?, 293 | ) 294 | .map_err(DiscoveryError::Request) 295 | .and_then(|http_response| { 296 | Self::discovery_response(issuer_url, &discovery_url, http_response) 297 | }) 298 | .and_then(|provider_metadata| { 299 | JsonWebKeySet::fetch(provider_metadata.jwks_uri(), http_client).map(|jwks| Self { 300 | jwks, 301 | ..provider_metadata 302 | }) 303 | }) 304 | } 305 | 306 | /// Asynchronously fetches the OpenID Connect Discovery document and associated JSON Web Key Set 307 | /// from the OpenID Connect Provider. 308 | pub fn discover_async<'c, C>( 309 | issuer_url: IssuerUrl, 310 | http_client: &'c C, 311 | ) -> impl Future>::Error>>> + 'c 312 | where 313 | Self: 'c, 314 | C: AsyncHttpClient<'c>, 315 | { 316 | Box::pin(async move { 317 | let discovery_url = issuer_url 318 | .join(CONFIG_URL_SUFFIX) 319 | .map_err(DiscoveryError::UrlParse)?; 320 | 321 | let provider_metadata = http_client 322 | .call( 323 | Self::discovery_request(discovery_url.clone()).map_err(|err| { 324 | DiscoveryError::Other(format!("failed to prepare request: {err}")) 325 | })?, 326 | ) 327 | .await 328 | .map_err(DiscoveryError::Request) 329 | .and_then(|http_response| { 330 | Self::discovery_response(&issuer_url, &discovery_url, http_response) 331 | })?; 332 | 333 | JsonWebKeySet::fetch_async(provider_metadata.jwks_uri(), http_client) 334 | .await 335 | .map(|jwks| Self { 336 | jwks, 337 | ..provider_metadata 338 | }) 339 | }) 340 | } 341 | 342 | fn discovery_request(discovery_url: url::Url) -> Result { 343 | http::Request::builder() 344 | .uri(discovery_url.to_string()) 345 | .method(Method::GET) 346 | .header(ACCEPT, HeaderValue::from_static(MIME_TYPE_JSON)) 347 | .body(Vec::new()) 348 | } 349 | 350 | fn discovery_response( 351 | issuer_url: &IssuerUrl, 352 | discovery_url: &url::Url, 353 | discovery_response: HttpResponse, 354 | ) -> Result> 355 | where 356 | RE: std::error::Error + 'static, 357 | { 358 | if discovery_response.status() != StatusCode::OK { 359 | return Err(DiscoveryError::Response( 360 | discovery_response.status(), 361 | discovery_response.body().to_owned(), 362 | format!( 363 | "HTTP status code {} at {}", 364 | discovery_response.status(), 365 | discovery_url 366 | ), 367 | )); 368 | } 369 | 370 | check_content_type(discovery_response.headers(), MIME_TYPE_JSON).map_err(|err_msg| { 371 | DiscoveryError::Response( 372 | discovery_response.status(), 373 | discovery_response.body().to_owned(), 374 | err_msg, 375 | ) 376 | })?; 377 | 378 | let provider_metadata = serde_path_to_error::deserialize::<_, Self>( 379 | &mut serde_json::Deserializer::from_slice(discovery_response.body()), 380 | ) 381 | .map_err(DiscoveryError::Parse)?; 382 | 383 | if provider_metadata.issuer() != issuer_url { 384 | Err(DiscoveryError::Validation(format!( 385 | "unexpected issuer URI `{}` (expected `{}`)", 386 | provider_metadata.issuer().as_str(), 387 | issuer_url.as_str() 388 | ))) 389 | } else { 390 | Ok(provider_metadata) 391 | } 392 | } 393 | 394 | /// Returns additional provider metadata fields. 395 | pub fn additional_metadata(&self) -> &A { 396 | &self.additional_metadata 397 | } 398 | /// Returns mutable additional provider metadata fields. 399 | pub fn additional_metadata_mut(&mut self) -> &mut A { 400 | &mut self.additional_metadata 401 | } 402 | } 403 | 404 | /// Error retrieving provider metadata. 405 | #[derive(Debug, Error)] 406 | #[non_exhaustive] 407 | pub enum DiscoveryError 408 | where 409 | RE: std::error::Error + 'static, 410 | { 411 | /// An unexpected error occurred. 412 | #[error("Other error: {0}")] 413 | Other(String), 414 | /// Failed to parse server response. 415 | #[error("Failed to parse server response")] 416 | Parse(#[source] serde_path_to_error::Error), 417 | /// An error occurred while sending the request or receiving the response (e.g., network 418 | /// connectivity failed). 419 | #[error("Request failed")] 420 | Request(#[source] RE), 421 | /// Server returned an invalid response. 422 | #[error("Server returned invalid response: {2}")] 423 | Response(StatusCode, Vec, String), 424 | /// Failed to parse discovery URL from issuer URL. 425 | #[error("Failed to parse URL")] 426 | UrlParse(#[source] url::ParseError), 427 | /// Failed to validate provider metadata. 428 | #[error("Validation error: {0}")] 429 | Validation(String), 430 | } 431 | -------------------------------------------------------------------------------- /src/claims.rs: -------------------------------------------------------------------------------- 1 | use crate::helpers::{Boolean, DeserializeMapField, FlattenFilter, Timestamp}; 2 | use crate::types::localized::split_language_tag_key; 3 | use crate::{ 4 | AddressCountry, AddressLocality, AddressPostalCode, AddressRegion, EndUserBirthday, 5 | EndUserEmail, EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, 6 | EndUserNickname, EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, 7 | EndUserUsername, EndUserWebsiteUrl, FormattedAddress, LanguageTag, LocalizedClaim, 8 | StreetAddress, SubjectIdentifier, 9 | }; 10 | 11 | use chrono::{DateTime, Utc}; 12 | use serde::de::{DeserializeOwned, Deserializer, MapAccess, Visitor}; 13 | use serde::ser::SerializeMap; 14 | use serde::{Deserialize, Serialize, Serializer}; 15 | use serde_with::skip_serializing_none; 16 | 17 | use std::fmt::{Debug, Formatter, Result as FormatterResult}; 18 | use std::marker::PhantomData; 19 | use std::str; 20 | 21 | /// Additional claims beyond the set of Standard Claims defined by OpenID Connect Core. 22 | pub trait AdditionalClaims: Debug + DeserializeOwned + Serialize + 'static {} 23 | 24 | /// No additional claims. 25 | #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] 26 | // In order to support serde flatten, this must be an empty struct rather than an empty 27 | // tuple struct. 28 | pub struct EmptyAdditionalClaims {} 29 | impl AdditionalClaims for EmptyAdditionalClaims {} 30 | 31 | /// Address claims. 32 | #[skip_serializing_none] 33 | #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] 34 | pub struct AddressClaim { 35 | /// Full mailing address, formatted for display or use on a mailing label. 36 | /// 37 | /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented 38 | /// either as a carriage return/line feed pair (`\r\n`) or as a single line feed character 39 | /// (`\n`). 40 | pub formatted: Option, 41 | /// Full street address component, which MAY include house number, street name, Post Office Box, 42 | /// and multi-line extended street address information. 43 | /// 44 | /// This field MAY contain multiple lines, separated by newlines. Newlines can be represented 45 | /// either as a carriage return/line feed pair (`\r\n`) or as a single line feed character 46 | /// (`\n`). 47 | pub street_address: Option, 48 | /// City or locality component. 49 | pub locality: Option, 50 | /// State, province, prefecture, or region component. 51 | pub region: Option, 52 | /// Zip code or postal code component. 53 | pub postal_code: Option, 54 | /// Country name component. 55 | pub country: Option, 56 | } 57 | 58 | /// Gender claim. 59 | pub trait GenderClaim: Clone + Debug + DeserializeOwned + Serialize + 'static {} 60 | 61 | /// Standard Claims defined by OpenID Connect Core. 62 | #[derive(Clone, Debug, PartialEq, Eq)] 63 | pub struct StandardClaims 64 | where 65 | GC: GenderClaim, 66 | { 67 | pub(crate) sub: SubjectIdentifier, 68 | pub(crate) name: Option>, 69 | pub(crate) given_name: Option>, 70 | pub(crate) family_name: Option>, 71 | pub(crate) middle_name: Option>, 72 | pub(crate) nickname: Option>, 73 | pub(crate) preferred_username: Option, 74 | pub(crate) profile: Option>, 75 | pub(crate) picture: Option>, 76 | pub(crate) website: Option>, 77 | pub(crate) email: Option, 78 | pub(crate) email_verified: Option, 79 | pub(crate) gender: Option, 80 | pub(crate) birthday: Option, 81 | pub(crate) birthdate: Option, 82 | pub(crate) zoneinfo: Option, 83 | pub(crate) locale: Option, 84 | pub(crate) phone_number: Option, 85 | pub(crate) phone_number_verified: Option, 86 | pub(crate) address: Option, 87 | pub(crate) updated_at: Option>, 88 | } 89 | impl StandardClaims 90 | where 91 | GC: GenderClaim, 92 | { 93 | /// Initializes a set of Standard Claims. 94 | /// 95 | /// The Subject (`sub`) claim is the only required Standard Claim. 96 | pub fn new(subject: SubjectIdentifier) -> Self { 97 | Self { 98 | sub: subject, 99 | name: None, 100 | given_name: None, 101 | family_name: None, 102 | middle_name: None, 103 | nickname: None, 104 | preferred_username: None, 105 | profile: None, 106 | picture: None, 107 | website: None, 108 | email: None, 109 | email_verified: None, 110 | gender: None, 111 | birthday: None, 112 | birthdate: None, 113 | zoneinfo: None, 114 | locale: None, 115 | phone_number: None, 116 | phone_number_verified: None, 117 | address: None, 118 | updated_at: None, 119 | } 120 | } 121 | 122 | /// Returns the Subject (`sub`) claim. 123 | pub fn subject(&self) -> &SubjectIdentifier { 124 | &self.sub 125 | } 126 | 127 | /// Sets the Subject (`sub`) claim. 128 | pub fn set_subject(mut self, subject: SubjectIdentifier) -> Self { 129 | self.sub = subject; 130 | self 131 | } 132 | 133 | field_getters_setters![ 134 | pub self [self] ["claim"] { 135 | set_name -> name[Option>], 136 | set_given_name -> given_name[Option>], 137 | set_family_name -> 138 | family_name[Option>], 139 | set_middle_name -> 140 | middle_name[Option>], 141 | set_nickname -> nickname[Option>], 142 | set_preferred_username -> preferred_username[Option], 143 | set_profile -> profile[Option>], 144 | set_picture -> picture[Option>], 145 | set_website -> website[Option>], 146 | set_email -> email[Option], 147 | set_email_verified -> email_verified[Option], 148 | set_gender -> gender[Option], 149 | set_birthday -> birthday[Option], 150 | set_birthdate -> birthdate[Option], 151 | set_zoneinfo -> zoneinfo[Option], 152 | set_locale -> locale[Option], 153 | set_phone_number -> phone_number[Option], 154 | set_phone_number_verified -> phone_number_verified[Option], 155 | set_address -> address[Option], 156 | set_updated_at -> updated_at[Option>], 157 | } 158 | ]; 159 | } 160 | impl FlattenFilter for StandardClaims 161 | where 162 | GC: GenderClaim, 163 | { 164 | // When another struct (i.e., additional claims) is co-flattened with this one, only include 165 | // fields in that other struct which are not part of this struct. 166 | fn should_include(field_name: &str) -> bool { 167 | !matches!( 168 | split_language_tag_key(field_name), 169 | ("sub", None) 170 | | ("name", _) 171 | | ("given_name", _) 172 | | ("family_name", _) 173 | | ("middle_name", _) 174 | | ("nickname", _) 175 | | ("preferred_username", None) 176 | | ("profile", _) 177 | | ("picture", _) 178 | | ("website", _) 179 | | ("email", None) 180 | | ("email_verified", None) 181 | | ("gender", None) 182 | | ("birthday", None) 183 | | ("birthdate", None) 184 | | ("zoneinfo", None) 185 | | ("locale", None) 186 | | ("phone_number", None) 187 | | ("phone_number_verified", None) 188 | | ("address", None) 189 | | ("updated_at", None) 190 | ) 191 | } 192 | } 193 | impl<'de, GC> Deserialize<'de> for StandardClaims 194 | where 195 | GC: GenderClaim, 196 | { 197 | /// Special deserializer that supports [RFC 5646](https://tools.ietf.org/html/rfc5646) language 198 | /// tags associated with human-readable client metadata fields. 199 | fn deserialize(deserializer: D) -> Result 200 | where 201 | D: Deserializer<'de>, 202 | { 203 | struct ClaimsVisitor(PhantomData); 204 | impl<'de, GC> Visitor<'de> for ClaimsVisitor 205 | where 206 | GC: GenderClaim, 207 | { 208 | type Value = StandardClaims; 209 | 210 | fn expecting(&self, formatter: &mut Formatter) -> FormatterResult { 211 | formatter.write_str("struct StandardClaims") 212 | } 213 | fn visit_map(self, mut map: V) -> Result 214 | where 215 | V: MapAccess<'de>, 216 | { 217 | // NB: The non-localized fields are actually Option> here so that we can 218 | // distinguish between omitted fields and fields explicitly set to `null`. The 219 | // latter is necessary so that we can detect duplicate fields (e.g., if a key is 220 | // present both with a null value and a non-null value, that's an error). 221 | let mut sub = None; 222 | let mut name = None; 223 | let mut given_name = None; 224 | let mut family_name = None; 225 | let mut middle_name = None; 226 | let mut nickname = None; 227 | let mut preferred_username = None; 228 | let mut profile = None; 229 | let mut picture = None; 230 | let mut website = None; 231 | let mut email = None; 232 | let mut email_verified = None; 233 | let mut gender = None; 234 | let mut birthday = None; 235 | let mut birthdate = None; 236 | let mut zoneinfo = None; 237 | let mut locale = None; 238 | let mut phone_number = None; 239 | let mut phone_number_verified = None; 240 | let mut address = None; 241 | let mut updated_at = None; 242 | 243 | macro_rules! field_case { 244 | ($field:ident, $typ:ty, $language_tag:ident) => {{ 245 | $field = Some(<$typ>::deserialize_map_field( 246 | &mut map, 247 | stringify!($field), 248 | $language_tag, 249 | $field, 250 | )?); 251 | }}; 252 | } 253 | 254 | while let Some(key) = map.next_key::()? { 255 | let (field_name, language_tag) = split_language_tag_key(&key); 256 | match field_name { 257 | "sub" => field_case!(sub, SubjectIdentifier, language_tag), 258 | "name" => field_case!(name, LocalizedClaim>, language_tag), 259 | "given_name" => { 260 | field_case!(given_name, LocalizedClaim>, language_tag) 261 | } 262 | "family_name" => { 263 | field_case!(family_name, LocalizedClaim>, language_tag) 264 | } 265 | "middle_name" => { 266 | field_case!(middle_name, LocalizedClaim>, language_tag) 267 | } 268 | "nickname" => { 269 | field_case!(nickname, LocalizedClaim>, language_tag) 270 | } 271 | "preferred_username" => { 272 | field_case!(preferred_username, Option<_>, language_tag) 273 | } 274 | "profile" => { 275 | field_case!(profile, LocalizedClaim>, language_tag) 276 | } 277 | "picture" => { 278 | field_case!(picture, LocalizedClaim>, language_tag) 279 | } 280 | "website" => { 281 | field_case!(website, LocalizedClaim>, language_tag) 282 | } 283 | "email" => field_case!(email, Option<_>, language_tag), 284 | "email_verified" => { 285 | field_case!(email_verified, Option, language_tag) 286 | } 287 | "gender" => field_case!(gender, Option<_>, language_tag), 288 | "birthday" => field_case!(birthday, Option<_>, language_tag), 289 | "birthdate" => field_case!(birthdate, Option<_>, language_tag), 290 | "zoneinfo" => field_case!(zoneinfo, Option<_>, language_tag), 291 | "locale" => field_case!(locale, Option<_>, language_tag), 292 | "phone_number" => field_case!(phone_number, Option<_>, language_tag), 293 | "phone_number_verified" => { 294 | field_case!(phone_number_verified, Option, language_tag) 295 | } 296 | "address" => field_case!(address, Option<_>, language_tag), 297 | "updated_at" => field_case!(updated_at, Option, language_tag), 298 | // Ignore unknown fields. 299 | _ => { 300 | map.next_value::()?; 301 | continue; 302 | } 303 | }; 304 | } 305 | 306 | Ok(StandardClaims { 307 | sub: sub.ok_or_else(|| serde::de::Error::missing_field("sub"))?, 308 | name: name.and_then(LocalizedClaim::flatten_or_none), 309 | given_name: given_name.and_then(LocalizedClaim::flatten_or_none), 310 | family_name: family_name.and_then(LocalizedClaim::flatten_or_none), 311 | middle_name: middle_name.and_then(LocalizedClaim::flatten_or_none), 312 | nickname: nickname.and_then(LocalizedClaim::flatten_or_none), 313 | preferred_username: preferred_username.flatten(), 314 | profile: profile.and_then(LocalizedClaim::flatten_or_none), 315 | picture: picture.and_then(LocalizedClaim::flatten_or_none), 316 | website: website.and_then(LocalizedClaim::flatten_or_none), 317 | email: email.flatten(), 318 | email_verified: email_verified.flatten().map(Boolean::into_inner), 319 | gender: gender.flatten(), 320 | birthday: birthday.flatten(), 321 | birthdate: birthdate.flatten(), 322 | zoneinfo: zoneinfo.flatten(), 323 | locale: locale.flatten(), 324 | phone_number: phone_number.flatten(), 325 | phone_number_verified: phone_number_verified.flatten().map(Boolean::into_inner), 326 | address: address.flatten(), 327 | updated_at: updated_at 328 | .flatten() 329 | .map(|sec| { 330 | sec.to_utc().map_err(|_| { 331 | serde::de::Error::custom(format!( 332 | "failed to parse `{sec}` as UTC datetime (in seconds) for key \ 333 | `updated_at`" 334 | )) 335 | }) 336 | }) 337 | .transpose()?, 338 | }) 339 | } 340 | } 341 | deserializer.deserialize_map(ClaimsVisitor(PhantomData)) 342 | } 343 | } 344 | impl Serialize for StandardClaims 345 | where 346 | GC: GenderClaim, 347 | { 348 | #[allow(clippy::cognitive_complexity)] 349 | fn serialize(&self, serializer: SE) -> Result 350 | where 351 | SE: Serializer, 352 | { 353 | serialize_fields! { 354 | self -> serializer { 355 | [sub] 356 | [LanguageTag(name)] 357 | [LanguageTag(given_name)] 358 | [LanguageTag(family_name)] 359 | [LanguageTag(middle_name)] 360 | [LanguageTag(nickname)] 361 | [Option(preferred_username)] 362 | [LanguageTag(profile)] 363 | [LanguageTag(picture)] 364 | [LanguageTag(website)] 365 | [Option(email)] 366 | [Option(email_verified)] 367 | [Option(gender)] 368 | [Option(birthday)] 369 | [Option(birthdate)] 370 | [Option(zoneinfo)] 371 | [Option(locale)] 372 | [Option(phone_number)] 373 | [Option(phone_number_verified)] 374 | [Option(address)] 375 | [Option(DateTime(Seconds(updated_at)))] 376 | } 377 | } 378 | } 379 | } 380 | 381 | #[cfg(test)] 382 | mod tests { 383 | use crate::core::CoreGenderClaim; 384 | use crate::StandardClaims; 385 | 386 | // The spec states (https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse): 387 | // "If a Claim is not returned, that Claim Name SHOULD be omitted from the JSON object 388 | // representing the Claims; it SHOULD NOT be present with a null or empty string value." 389 | // However, we still aim to support identity providers that disregard this suggestion. 390 | #[test] 391 | fn test_null_optional_claims() { 392 | let claims = serde_json::from_str::>( 393 | r#"{ 394 | "sub": "24400320", 395 | "name": null, 396 | "given_name": null, 397 | "family_name": null, 398 | "middle_name": null, 399 | "nickname": null, 400 | "preferred_username": null, 401 | "profile": null, 402 | "picture": null, 403 | "website": null, 404 | "email": null, 405 | "email_verified": null, 406 | "gender": null, 407 | "birthday": null, 408 | "birthdate": null, 409 | "zoneinfo": null, 410 | "locale": null, 411 | "phone_number": null, 412 | "phone_number_verified": null, 413 | "address": null, 414 | "updated_at": null 415 | }"#, 416 | ) 417 | .expect("should deserialize successfully"); 418 | 419 | assert_eq!(claims.subject().as_str(), "24400320"); 420 | assert_eq!(claims.name(), None); 421 | } 422 | 423 | fn expect_err_prefix( 424 | result: Result, serde_json::Error>, 425 | expected_prefix: &str, 426 | ) { 427 | let err_str = result.expect_err("deserialization should fail").to_string(); 428 | assert!( 429 | err_str.starts_with(expected_prefix), 430 | "error message should begin with `{}`: {}", 431 | expected_prefix, 432 | err_str, 433 | ) 434 | } 435 | 436 | #[test] 437 | fn test_duplicate_claims() { 438 | expect_err_prefix( 439 | serde_json::from_str( 440 | r#"{ 441 | "sub": "24400320", 442 | "sub": "24400321" 443 | }"#, 444 | ), 445 | "duplicate field `sub` at line", 446 | ); 447 | 448 | expect_err_prefix( 449 | serde_json::from_str( 450 | r#"{ 451 | "name": null, 452 | "sub": "24400320", 453 | "name": "foo", 454 | }"#, 455 | ), 456 | "duplicate field `name` at line", 457 | ); 458 | 459 | expect_err_prefix( 460 | serde_json::from_str( 461 | r#"{ 462 | "name#en": null, 463 | "sub": "24400320", 464 | "name#en": "foo", 465 | }"#, 466 | ), 467 | "duplicate field `name#en` at line", 468 | ); 469 | } 470 | 471 | #[test] 472 | fn test_err_field_name() { 473 | expect_err_prefix( 474 | serde_json::from_str( 475 | r#"{ 476 | "sub": 24400320 477 | }"#, 478 | ), 479 | "sub: invalid type: integer `24400320`, expected a string at line", 480 | ); 481 | } 482 | } 483 | --------------------------------------------------------------------------------