lichess_api/model/oauth/pkce.rs
1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use rand::RngExt;
4use sha2::{Digest, Sha256};
5
6/// Number of random bytes used for generated secrets.
7///
8/// 32 bytes base64url-encodes to 43 characters, the minimum length RFC 7636
9/// permits for a `code_verifier`.
10const SECRET_BYTES: usize = 32;
11
12/// Generate a cryptographically random, base64url-encoded secret.
13fn generate_secret() -> String {
14 let bytes: [u8; SECRET_BYTES] = rand::rng().random();
15 URL_SAFE_NO_PAD.encode(bytes)
16}
17
18/// A PKCE secret pair, as described by RFC 7636.
19///
20/// The `verifier` is kept private until the token exchange; the `challenge` is
21/// what gets sent in the authorization request. Only the challenge travels over
22/// the initial redirect, so an eavesdropper who intercepts the authorization
23/// code cannot exchange it without the verifier.
24///
25/// Keep the verifier out of URLs and off insecure connections. For fully
26/// client-side apps the user themselves can always extract it, which is fine.
27#[derive(Clone, Debug)]
28pub struct Pkce {
29 verifier: String,
30 challenge: String,
31}
32
33impl Pkce {
34 /// Generate a new random verifier and its derived challenge.
35 pub fn generate() -> Self {
36 let verifier = generate_secret();
37 let challenge = Self::derive_challenge(&verifier);
38
39 Self {
40 verifier,
41 challenge,
42 }
43 }
44
45 /// `BASE64URL(SHA256(code_verifier))`, the `S256` challenge method.
46 pub fn derive_challenge(verifier: &str) -> String {
47 URL_SAFE_NO_PAD.encode(Sha256::digest(verifier.as_bytes()))
48 }
49
50 /// The secret sent only in the token exchange.
51 pub fn verifier(&self) -> &str {
52 &self.verifier
53 }
54
55 /// The derived value sent in the authorization request.
56 pub fn challenge(&self) -> &str {
57 &self.challenge
58 }
59}
60
61/// Generate a random `state` value, used to tie an authorization result back to
62/// the request that started it and defend against cross site request forgery.
63pub fn generate_state() -> String {
64 generate_secret()
65}