Skip to main content

axum_security_oauth2/
lib.rs

1//! A minimal OAuth2 client for the authorization code flow (RFC 6749),
2//! written for the [axum-security](https://crates.io/crates/axum-security)
3//! family but usable on its own.
4//!
5//! Compared to the `oauth2` crate this one has no typestate and no type
6//! parameters: configuration is validated once at
7//! [`build()`](OAuth2ClientBuilder::build), so client calls only fail for
8//! reasons that can occur at request time. PKCE (RFC 7636) is on for the
9//! default [`start_login`](OAuth2Client::start_login)/
10//! [`finish_login`](OAuth2Client::finish_login) pair; explicit `_non_pkce`
11//! variants exist for providers that reject the PKCE parameters.
12//! Values are plain `String`, [`url::Url`] or [`std::time::Duration`], with
13//! one wrapper: [`CsrfToken`], whose `==` compares in constant time.
14//! Secrets stay out of logs because every crate type that holds one (the
15//! client, [`Login`], [`Tokens`], [`CsrfToken`], errors) redacts it in its
16//! `Debug` output — but a secret *you* store is a plain string, so keep it
17//! out of your own `Debug`/`Display` impls.
18//!
19//! # Features
20//!
21//! - `reqwest` *(default)* — the [`reqwest`] backend for the [`HttpClient`]
22//!   enum, plus a default client (no redirects, 10s timeout). Without any
23//!   backend feature [`try_build`](OAuth2ClientBuilder::try_build) fails
24//!   with [`ConfigError::NoHttpClient`].
25//! - `rustls` *(default)* — TLS for the reqwest backend via rustls.
26//! - `native-tls` — TLS for the reqwest backend via the platform's native
27//!   TLS library.
28//!
29//! # Example
30//!
31//! ```no_run
32//! use axum_security_oauth2::OAuth2Client;
33//!
34//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
35//! // Provider shortcuts (github, google, microsoft, gitlab, discord, spotify, twitch)
36//! // preset the endpoints; OAuth2Client::builder() takes them explicitly.
37//! let client = OAuth2Client::github()
38//!     .client_id("my-client-id")
39//!     .client_secret("my-client-secret")
40//!     .redirect_url("https://my-app.example/callback")
41//!     .scopes(&["read:user"])
42//!     .build(); // or try_build() to handle ConfigError
43//!
44//! // Leg 1: redirect the user to `login.url`; persist the CSRF token
45//! // and PKCE verifier (e.g. in a signed cookie) for the callback.
46//! let login = client.start_login();
47//! // Fields are owned and public — move them out, no clone needed.
48//! let (url, csrf_token, pkce_verifier) = (login.url, login.csrf_token, login.pkce_verifier);
49//!
50//! // Leg 2 (on the callback route): compare `csrf_token` with the `state`
51//! // query parameter (constant-time via `==`), then exchange the code.
52//! let state = "state-from-the-query-string";
53//! assert!(csrf_token == state); // reject the callback if this fails
54//! let code = "code-from-the-query-string";
55//! let tokens = client.finish_login(code, &pkce_verifier).await?;
56//! let _access_token = &tokens.access_token;
57//!
58//! // Later: trade the refresh token for fresh tokens (RFC 6749 §6).
59//! if let Some(refresh_token) = &tokens.refresh_token {
60//!     let fresh = client.refresh_tokens(refresh_token).await?;
61//!     let _fresh_access_token = &fresh.access_token;
62//! }
63//! # Ok(())
64//! # }
65//! ```
66//!
67//! Per-login extras (an oidc `nonce`, `prompt`, ...) go through
68//! [`start_login_with`](OAuth2Client::start_login_with); providers that
69//! only take credentials in the request body are served by
70//! [`AuthType::RequestBody`].
71
72mod builder;
73mod client;
74mod csrf;
75mod error;
76mod http;
77mod login;
78mod pkce;
79mod rand;
80mod tokens;
81
82pub use builder::{ConfigError, OAuth2ClientBuilder};
83pub use client::{AuthType, OAuth2Client};
84pub use csrf::CsrfToken;
85pub use error::{Error, ErrorCode, HttpError, ParseError, ServerError};
86pub use http::{HttpClient, HttpResponse};
87pub use login::{Login, LoginNonPkce, LoginOptions};
88pub use rand::random_token;
89pub use tokens::Tokens;