Skip to main content

lichess_api/model/oauth/
authorize.rs

1use super::PendingAuthorization;
2use super::pkce::{Pkce, generate_state};
3use crate::error::Result;
4use crate::model::Domain;
5use serde::Serialize;
6use serde_with::skip_serializing_none;
7
8/// Parameters for the OAuth2 authorization endpoint.
9///
10/// This endpoint is not called by this library: it renders an authorization
11/// prompt for the user in a browser, and the result is delivered as query
12/// parameters appended to your `redirect_uri`.
13///
14/// `response_type` and `code_challenge_method` are fixed by the spec and are
15/// set for you.
16///
17/// # Example
18///
19/// ```no_run
20/// use lichess_api::model::oauth::authorize::AuthorizationUrl;
21///
22/// # fn main() -> lichess_api::error::Result<()> {
23/// let (url, pending) = AuthorizationUrl::generated("example.com", "http://example.com/")
24///     .scope("preference:read")
25///     .start()?;
26///
27/// // Send the user to `url`, and keep `pending` until they are redirected back.
28/// # Ok(())
29/// # }
30/// ```
31///
32/// [`AuthorizationUrl::start`] generates the PKCE secrets and the `state` for
33/// you, and returns a [`PendingAuthorization`] that verifies the result and
34/// completes the exchange. Use [`AuthorizationUrl::new`] with
35/// [`AuthorizationUrl::to_url`] only if you are managing those secrets
36/// yourself, and keep the `code_verifier` out of URLs and off insecure
37/// connections.
38#[skip_serializing_none]
39#[derive(Clone, Debug, Serialize)]
40pub struct AuthorizationUrl {
41    response_type: &'static str,
42    /// Arbitrary identifier that uniquely identifies your application.
43    pub client_id: String,
44    /// The absolute URL the user should be redirected to with the result.
45    pub redirect_uri: String,
46    code_challenge_method: &'static str,
47    /// `BASE64URL(SHA256(code_verifier))`.
48    pub code_challenge: String,
49    /// Space separated list of requested OAuth scopes, if any.
50    pub scope: Option<String>,
51    /// Hint that the user should log in with a specific Lichess username.
52    pub username: Option<String>,
53    /// Arbitrary state returned verbatim with the authorization result.
54    pub state: Option<String>,
55}
56
57impl AuthorizationUrl {
58    /// Start an authorization request whose PKCE secrets and `state` are
59    /// generated for you by [`AuthorizationUrl::start`].
60    ///
61    /// Prefer this over [`AuthorizationUrl::new`] unless you are managing the
62    /// PKCE secrets yourself.
63    pub fn generated(client_id: impl Into<String>, redirect_uri: impl Into<String>) -> Self {
64        Self::new(client_id, redirect_uri, String::new())
65    }
66
67    /// Build an authorization request from a `code_challenge` you computed
68    /// yourself.
69    ///
70    /// The challenge is `BASE64URL(SHA256(code_verifier))`; see
71    /// [`Pkce::derive_challenge`]. Most callers should use
72    /// [`AuthorizationUrl::generated`] with [`AuthorizationUrl::start`] instead.
73    pub fn new(
74        client_id: impl Into<String>,
75        redirect_uri: impl Into<String>,
76        code_challenge: impl Into<String>,
77    ) -> Self {
78        Self {
79            response_type: "code",
80            client_id: client_id.into(),
81            redirect_uri: redirect_uri.into(),
82            code_challenge_method: "S256",
83            code_challenge: code_challenge.into(),
84            scope: None,
85            username: None,
86            state: None,
87        }
88    }
89
90    /// Space separated list of requested OAuth scopes.
91    pub fn scope(mut self, scope: impl Into<String>) -> Self {
92        self.scope = Some(scope.into());
93        self
94    }
95
96    /// Hint that the user should log in with a specific Lichess username.
97    pub fn username(mut self, username: impl Into<String>) -> Self {
98        self.username = Some(username.into());
99        self
100    }
101
102    /// Arbitrary state returned verbatim with the authorization result.
103    pub fn state(mut self, state: impl Into<String>) -> Self {
104        self.state = Some(state.into());
105        self
106    }
107
108    /// Begin an authorization request, generating the PKCE secrets and `state`
109    /// for you.
110    ///
111    /// Returns the URL to send the user to, and a [`PendingAuthorization`]
112    /// holding the secrets needed to complete the flow. Store the pending value
113    /// for the duration of the request (in session storage for a web backend,
114    /// in memory for a native app) and finish with
115    /// [`PendingAuthorization::complete`].
116    ///
117    /// Any `state` set on this builder is replaced by a freshly generated one.
118    /// Use [`PendingAuthorization::new`] directly if you must supply your own.
119    pub fn start(mut self) -> Result<(url::Url, PendingAuthorization)> {
120        let pkce = Pkce::generate();
121        let state = generate_state();
122
123        self.code_challenge = pkce.challenge().to_string();
124        self.state = Some(state.clone());
125
126        let url = self.to_url()?;
127        let pending =
128            PendingAuthorization::new(pkce.verifier(), state, self.client_id, self.redirect_uri);
129
130        Ok((url, pending))
131    }
132
133    /// Build the URL to send the user to in order to grant authorization.
134    pub fn to_url(&self) -> Result<url::Url> {
135        let base_url = format!("https://{}", Domain::Lichess.as_ref());
136        let mut url = url::Url::parse(&base_url).expect("invalid base url");
137
138        {
139            let mut query_pairs = url.query_pairs_mut();
140            let query_serializer = serde_urlencoded::Serializer::new(&mut query_pairs);
141            self.serialize(query_serializer)?;
142        }
143
144        url.set_path("/oauth");
145
146        Ok(url)
147    }
148}