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}