Crate openidconnect

source ·
Expand description

OpenID Connect library.

This library provides extensible, strongly-typed interfaces for the OpenID Connect protocol. For convenience, the core module provides type aliases for common usage that adheres to the OpenID Connect Core spec. Users of this crate may define their own extensions and custom type parameters in lieu of using the core module.

§Contents

§Importing openidconnect: selecting an HTTP client interface

This library offers a flexible HTTP client interface with two modes:

  • Synchronous (blocking)
  • Asynchronous

For the HTTP client modes described above, the following HTTP client implementations can be used:

  • reqwest

    The reqwest HTTP client supports both the synchronous and asynchronous modes and is enabled by default.

    Synchronous client: reqwest::http_client

    Asynchronous client: reqwest::async_http_client

  • curl

    The curl HTTP client only supports the synchronous HTTP client mode and can be enabled in Cargo.toml via the curl feature flag.

    Synchronous client: curl::http_client

  • Custom

    In addition to the clients above, users may define their own HTTP clients, which must accept an HttpRequest and return an HttpResponse or error. Users writing their own clients may wish to disable the default reqwest dependency by specifying default-features = false in Cargo.toml (replacing ... with the desired version of this crate):

    openidconnect = { version = "...", default-features = false }
    

    Synchronous HTTP clients should implement the following trait:

    FnOnce(HttpRequest) -> Result<HttpResponse, RE>
    where RE: std::error::Error + 'static
    

    Asynchronous HTTP clients should implement the following trait:

    FnOnce(HttpRequest) -> F
    where
      F: Future<Output = Result<HttpResponse, RE>>,
      RE: std::error::Error + 'static
    

§OpenID Connect Relying Party (Client) Interface

The Client struct provides the OpenID Connect Relying Party interface. The most common usage is provided by the core::CoreClient type alias.

§Examples

§Getting started: Authorization Code Grant w/ PKCE

This is the most common OIDC/OAuth2 flow. PKCE is recommended whenever the client has no client secret or has a client secret that cannot remain confidential (e.g., native, mobile, or client-side web applications).

§Example

use openidconnect::{
    AccessTokenHash,
    AuthenticationFlow,
    AuthorizationCode,
    ClientId,
    ClientSecret,
    CsrfToken,
    Nonce,
    IssuerUrl,
    PkceCodeChallenge,
    RedirectUrl,
    Scope,
};
use openidconnect::core::{
  CoreAuthenticationFlow,
  CoreClient,
  CoreProviderMetadata,
  CoreResponseType,
  CoreUserInfoClaims,
};
use anyhow::anyhow;

use openidconnect::reqwest::http_client;
use url::Url;

// Use OpenID Connect Discovery to fetch the provider metadata.
use openidconnect::{OAuth2TokenResponse, TokenResponse};
let provider_metadata = CoreProviderMetadata::discover(
    &IssuerUrl::new("https://accounts.example.com".to_string())?,
    http_client,
)?;

// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
// and token URL.
let client =
    CoreClient::from_provider_metadata(
        provider_metadata,
        ClientId::new("client_id".to_string()),
        Some(ClientSecret::new("client_secret".to_string())),
    )
    // Set the URL the user will be redirected to after the authorization process.
    .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?);

// Generate a PKCE challenge.
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

// Generate the full authorization URL.
let (auth_url, csrf_token, nonce) = client
    .authorize_url(
        CoreAuthenticationFlow::AuthorizationCode,
        CsrfToken::new_random,
        Nonce::new_random,
    )
    // Set the desired scopes.
    .add_scope(Scope::new("read".to_string()))
    .add_scope(Scope::new("write".to_string()))
    // Set the PKCE code challenge.
    .set_pkce_challenge(pkce_challenge)
    .url();

// This is the URL you should redirect the user to, in order to trigger the authorization
// process.
println!("Browse to: {}", auth_url);

// Once the user has been redirected to the redirect URL, you'll have access to the
// authorization code. For security reasons, your code should verify that the `state`
// parameter returned by the server matches `csrf_state`.

// Now you can exchange it for an access token and ID token.
let token_response =
    client
        .exchange_code(AuthorizationCode::new("some authorization code".to_string()))
        // Set the PKCE code verifier.
        .set_pkce_verifier(pkce_verifier)
        .request(http_client)?;

// Extract the ID token claims after verifying its authenticity and nonce.
let id_token = token_response
  .id_token()
  .ok_or_else(|| anyhow!("Server did not return an ID token"))?;
let claims = id_token.claims(&client.id_token_verifier(), &nonce)?;

// Verify the access token hash to ensure that the access token hasn't been substituted for
// another user's.
if let Some(expected_access_token_hash) = claims.access_token_hash() {
    let actual_access_token_hash = AccessTokenHash::from_token(
        token_response.access_token(),
        &id_token.signing_alg()?
    )?;
    if actual_access_token_hash != *expected_access_token_hash {
        return Err(anyhow!("Invalid access token"));
    }
}

// The authenticated user's identity is now available. See the IdTokenClaims struct for a
// complete listing of the available claims.
println!(
    "User {} with e-mail address {} has authenticated successfully",
    claims.subject().as_str(),
    claims.email().map(|email| email.as_str()).unwrap_or("<not provided>"),
);

// If available, we can use the UserInfo endpoint to request additional information.

// The user_info request uses the AccessToken returned in the token response. To parse custom
// claims, use UserInfoClaims directly (with the desired type parameters) rather than using the
// CoreUserInfoClaims type alias.
let userinfo: CoreUserInfoClaims = client
  .user_info(token_response.access_token().to_owned(), None)
  .map_err(|err| anyhow!("No user info endpoint: {:?}", err))?
  .request(http_client)
  .map_err(|err| anyhow!("Failed requesting user info: {:?}", err))?;

// See the OAuth2TokenResponse trait for a listing of other available fields such as
// access_token() and refresh_token().

§OpenID Connect Provider (Server) Interface

This library does not implement a complete OpenID Connect Provider, which requires functionality such as credential and session management. However, it does provide strongly-typed interfaces for parsing and building OpenID Connect protocol messages.

§OpenID Connect Discovery document

The ProviderMetadata struct implements the OpenID Connect Discovery document. This data structure should be serialized to JSON and served via the GET .well-known/openid-configuration path relative to your provider’s issuer URL.

§Example

use openidconnect::{
    AuthUrl,
    EmptyAdditionalProviderMetadata,
    IssuerUrl,
    JsonWebKeySetUrl,
    ResponseTypes,
    Scope,
    TokenUrl,
    UserInfoUrl,
};
use openidconnect::core::{
    CoreClaimName,
    CoreJwsSigningAlgorithm,
    CoreProviderMetadata,
    CoreResponseType,
    CoreSubjectIdentifierType
};
use url::Url;
use anyhow;

let provider_metadata = CoreProviderMetadata::new(
    // Parameters required by the OpenID Connect Discovery spec.
    IssuerUrl::new("https://accounts.example.com".to_string())?,
    AuthUrl::new("https://accounts.example.com/authorize".to_string())?,
    // Use the JsonWebKeySet struct to serve the JWK Set at this URL.
    JsonWebKeySetUrl::new("https://accounts.example.com/jwk".to_string())?,
    // Supported response types (flows).
    vec![
        // Recommended: support the code flow.
        ResponseTypes::new(vec![CoreResponseType::Code]),
        // Optional: support the implicit flow.
        ResponseTypes::new(vec![CoreResponseType::Token, CoreResponseType::IdToken])
        // Other flows including hybrid flows may also be specified here.
    ],
    // For user privacy, the Pairwise subject identifier type is preferred. This prevents
    // distinct relying parties (clients) from knowing whether their users represent the same
    // real identities. This identifier type is only useful for relying parties that don't
    // receive the 'email', 'profile' or other personally-identifying scopes.
    // The Public subject identifier type is also supported.
    vec![CoreSubjectIdentifierType::Pairwise],
    // Support the RS256 signature algorithm.
    vec![CoreJwsSigningAlgorithm::RsaSsaPssSha256],
    // OpenID Connect Providers may supply custom metadata by providing a struct that
    // implements the AdditionalProviderMetadata trait. This requires manually using the
    // generic ProviderMetadata struct rather than the CoreProviderMetadata type alias,
    // however.
    EmptyAdditionalProviderMetadata {},
)
// Specify the token endpoint (required for the code flow).
.set_token_endpoint(Some(TokenUrl::new("https://accounts.example.com/token".to_string())?))
// Recommended: support the UserInfo endpoint.
.set_userinfo_endpoint(
    Some(UserInfoUrl::new("https://accounts.example.com/userinfo".to_string())?)
)
// Recommended: specify the supported scopes.
.set_scopes_supported(Some(vec![
    Scope::new("openid".to_string()),
    Scope::new("email".to_string()),
    Scope::new("profile".to_string()),
]))
// Recommended: specify the supported ID token claims.
.set_claims_supported(Some(vec![
    // Providers may also define an enum instead of using CoreClaimName.
    CoreClaimName::new("sub".to_string()),
    CoreClaimName::new("aud".to_string()),
    CoreClaimName::new("email".to_string()),
    CoreClaimName::new("email_verified".to_string()),
    CoreClaimName::new("exp".to_string()),
    CoreClaimName::new("iat".to_string()),
    CoreClaimName::new("iss".to_string()),
    CoreClaimName::new("name".to_string()),
    CoreClaimName::new("given_name".to_string()),
    CoreClaimName::new("family_name".to_string()),
    CoreClaimName::new("picture".to_string()),
    CoreClaimName::new("locale".to_string()),
]));

serde_json::to_string(&provider_metadata).map_err(From::from)

§OpenID Connect Discovery JSON Web Key Set

The JSON Web Key Set (JWKS) provides the public keys that relying parties (clients) use to verify the authenticity of ID tokens returned by this OpenID Connect Provider. The JsonWebKeySet data structure should be serialized as JSON and served at the URL specified in the jwks_uri field of the ProviderMetadata returned in the OpenID Connect Discovery document.

§Example

use openidconnect::{JsonWebKeyId, PrivateSigningKey};
use openidconnect::core::{CoreJsonWebKey, CoreJsonWebKeySet, CoreRsaPrivateSigningKey};
use anyhow;

let jwks = CoreJsonWebKeySet::new(
    vec![
        // RSA keys may also be constructed directly using CoreJsonWebKey::new_rsa(). Providers
        // aiming to support other key types may provide their own implementation of the
        // JsonWebKey trait or submit a PR to add the desired support to this crate.
        CoreRsaPrivateSigningKey::from_pem(
            &rsa_pem,
            Some(JsonWebKeyId::new("key1".to_string()))
        )
        .expect("Invalid RSA private key")
        .as_verification_key()
    ]
);

serde_json::to_string(&jwks).map_err(From::from)

§OpenID Connect ID Token

The IdToken::new method is used for signing ID token claims, which can then be returned from the token endpoint as part of the StandardTokenResponse struct (or core::CoreTokenResponse type alias). The ID token can also be serialized to a string using the IdToken::to_string method and returned directly from the authorization endpoint when the implicit flow or certain hybrid flows are used. Note that in these flows, ID tokens must only be returned in the URL fragment, and never as a query parameter.

The ID token contains a combination of the OpenID Connect Standard Claims (see StandardClaims) and claims specific to the OpenID Connect ID Token (see IdTokenClaims).

§Example

use chrono::{Duration, Utc};
use openidconnect::{
    AccessToken,
    Audience,
    EmptyAdditionalClaims,
    EmptyExtraTokenFields,
    EndUserEmail,
    IssuerUrl,
    JsonWebKeyId,
    StandardClaims,
    SubjectIdentifier,
};
use openidconnect::core::{
    CoreIdToken,
    CoreIdTokenClaims,
    CoreIdTokenFields,
    CoreJwsSigningAlgorithm,
    CoreRsaPrivateSigningKey,
    CoreTokenResponse,
    CoreTokenType,
};
use anyhow;

let id_token = CoreIdToken::new(
    CoreIdTokenClaims::new(
        // Specify the issuer URL for the OpenID Connect Provider.
        IssuerUrl::new("https://accounts.example.com".to_string())?,
        // The audience is usually a single entry with the client ID of the client for whom
        // the ID token is intended. This is a required claim.
        vec![Audience::new("client-id-123".to_string())],
        // The ID token expiration is usually much shorter than that of the access or refresh
        // tokens issued to clients.
        Utc::now() + Duration::seconds(300),
        // The issue time is usually the current time.
        Utc::now(),
        // Set the standard claims defined by the OpenID Connect Core spec.
        StandardClaims::new(
            // Stable subject identifiers are recommended in place of e-mail addresses or other
            // potentially unstable identifiers. This is the only required claim.
            SubjectIdentifier::new("5f83e0ca-2b8e-4e8c-ba0a-f80fe9bc3632".to_string())
        )
        // Optional: specify the user's e-mail address. This should only be provided if the
        // client has been granted the 'profile' or 'email' scopes.
        .set_email(Some(EndUserEmail::new("bob@example.com".to_string())))
        // Optional: specify whether the provider has verified the user's e-mail address.
        .set_email_verified(Some(true)),
        // OpenID Connect Providers may supply custom claims by providing a struct that
        // implements the AdditionalClaims trait. This requires manually using the
        // generic IdTokenClaims struct rather than the CoreIdTokenClaims type alias,
        // however.
        EmptyAdditionalClaims {},
    ),
    // The private key used for signing the ID token. For confidential clients (those able
    // to maintain a client secret), a CoreHmacKey can also be used, in conjunction
    // with one of the CoreJwsSigningAlgorithm::HmacSha* signing algorithms. When using an
    // HMAC-based signing algorithm, the UTF-8 representation of the client secret should
    // be used as the HMAC key.
    &CoreRsaPrivateSigningKey::from_pem(
            &rsa_pem,
            Some(JsonWebKeyId::new("key1".to_string()))
        )
        .expect("Invalid RSA private key"),
    // Uses the RS256 signature algorithm. This crate supports any RS*, PS*, or HS*
    // signature algorithm.
    CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256,
    // When returning the ID token alongside an access token (e.g., in the Authorization Code
    // flow), it is recommended to pass the access token here to set the `at_hash` claim
    // automatically.
    Some(&access_token),
    // When returning the ID token alongside an authorization code (e.g., in the implicit
    // flow), it is recommended to pass the authorization code here to set the `c_hash` claim
    // automatically.
    None,
)?;

Ok(CoreTokenResponse::new(
    AccessToken::new("some_secret".to_string()),
    CoreTokenType::Bearer,
    CoreIdTokenFields::new(Some(id_token), EmptyExtraTokenFields {}),
))

§Asynchronous API

An asynchronous API for async/await is also provided.

§Example

use openidconnect::{
    AccessTokenHash,
    AuthenticationFlow,
    AuthorizationCode,
    ClientId,
    ClientSecret,
    CsrfToken,
    Nonce,
    IssuerUrl,
    PkceCodeChallenge,
    RedirectUrl,
    Scope,
};
use openidconnect::core::{
  CoreAuthenticationFlow,
  CoreClient,
  CoreProviderMetadata,
  CoreResponseType,
};
use openidconnect::reqwest::async_http_client;
use url::Url;
use anyhow::anyhow;


// Use OpenID Connect Discovery to fetch the provider metadata.
use openidconnect::{OAuth2TokenResponse, TokenResponse};
let provider_metadata = CoreProviderMetadata::discover_async(
    IssuerUrl::new("https://accounts.example.com".to_string())?,
    async_http_client,
)
.await?;

// Create an OpenID Connect client by specifying the client ID, client secret, authorization URL
// and token URL.
let client =
    CoreClient::from_provider_metadata(
        provider_metadata,
        ClientId::new("client_id".to_string()),
        Some(ClientSecret::new("client_secret".to_string())),
    )
    // Set the URL the user will be redirected to after the authorization process.
    .set_redirect_uri(RedirectUrl::new("http://redirect".to_string())?);

// Generate a PKCE challenge.
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

// Generate the full authorization URL.
let (auth_url, csrf_token, nonce) = client
    .authorize_url(
        CoreAuthenticationFlow::AuthorizationCode,
        CsrfToken::new_random,
        Nonce::new_random,
    )
    // Set the desired scopes.
    .add_scope(Scope::new("read".to_string()))
    .add_scope(Scope::new("write".to_string()))
    // Set the PKCE code challenge.
    .set_pkce_challenge(pkce_challenge)
    .url();

// This is the URL you should redirect the user to, in order to trigger the authorization
// process.
println!("Browse to: {}", auth_url);

// Once the user has been redirected to the redirect URL, you'll have access to the
// authorization code. For security reasons, your code should verify that the `state`
// parameter returned by the server matches `csrf_state`.

// Now you can exchange it for an access token and ID token.
let token_response =
    client
        .exchange_code(AuthorizationCode::new("some authorization code".to_string()))
        // Set the PKCE code verifier.
        .set_pkce_verifier(pkce_verifier)
        .request_async(async_http_client)
        .await?;

// Extract the ID token claims after verifying its authenticity and nonce.
let id_token = token_response
  .id_token()
  .ok_or_else(|| anyhow!("Server did not return an ID token"))?;
let claims = id_token.claims(&client.id_token_verifier(), &nonce)?;

// Verify the access token hash to ensure that the access token hasn't been substituted for
// another user's.
if let Some(expected_access_token_hash) = claims.access_token_hash() {
    let actual_access_token_hash = AccessTokenHash::from_token(
        token_response.access_token(),
        &id_token.signing_alg()?
    )?;
    if actual_access_token_hash != *expected_access_token_hash {
        return Err(anyhow!("Invalid access token"));
    }
}

// The authenticated user's identity is now available. See the IdTokenClaims struct for a
// complete listing of the available claims.
println!(
    "User {} with e-mail address {} has authenticated successfully",
    claims.subject().as_str(),
    claims.email().map(|email| email.as_str()).unwrap_or("<not provided>"),
);

// See the OAuth2TokenResponse trait for a listing of other available fields such as
// access_token() and refresh_token().

Re-exports§

  • pub use oauth2::http;
  • pub use oauth2::url;

Modules§

  • Baseline OpenID Connect implementation and types.
  • HTTP client backed by the curl crate. Requires “curl” feature.
  • OpenID Connect Dynamic Client Registration.
  • HTTP client backed by the reqwest crate. Requires “reqwest” feature.
  • HTTP client backed by the ureq crate. Requires “ureq” feature.

Structs§

Enums§

Traits§

Type Aliases§