Skip to main content

jacquard_oauth/
types.rs

1mod client_metadata;
2mod metadata;
3mod request;
4mod response;
5mod token;
6
7use crate::scopes::{ParseError, Scope, Scopes};
8
9pub use self::client_metadata::*;
10pub use self::metadata::*;
11pub use self::request::*;
12pub use self::response::*;
13pub use self::token::*;
14use jacquard_common::CowStr;
15use jacquard_common::IntoStatic;
16use jacquard_common::bos::{BosStr, DefaultStr};
17use jacquard_common::deps::fluent_uri::Uri;
18use serde::Deserialize;
19use smol_str::SmolStr;
20
21/// The `prompt` parameter for an OAuth authorization request.
22///
23/// Controls whether the authorization server prompts the user for
24/// re-authentication or re-consent, as defined in OpenID Connect Core ยง3.1.2.1.
25#[derive(Debug, Deserialize, Clone, Copy)]
26pub enum AuthorizeOptionPrompt {
27    /// Prompt the user to re-authenticate.
28    Login,
29    /// Do not display any authentication or consent UI; fail if interaction is required.
30    None,
31    /// Prompt the user for explicit consent before issuing tokens.
32    Consent,
33    /// Prompt the user to select an account when multiple sessions are active.
34    SelectAccount,
35}
36
37impl From<AuthorizeOptionPrompt> for CowStr<'static> {
38    fn from(value: AuthorizeOptionPrompt) -> Self {
39        CowStr::new_static(value.into())
40    }
41}
42
43impl From<AuthorizeOptionPrompt> for SmolStr {
44    fn from(value: AuthorizeOptionPrompt) -> Self {
45        SmolStr::new_static(value.into())
46    }
47}
48
49impl From<AuthorizeOptionPrompt> for &'static str {
50    fn from(value: AuthorizeOptionPrompt) -> Self {
51        match value {
52            AuthorizeOptionPrompt::Login => "login",
53            AuthorizeOptionPrompt::None => "none",
54            AuthorizeOptionPrompt::Consent => "consent",
55            AuthorizeOptionPrompt::SelectAccount => "select_account",
56        }
57    }
58}
59
60/// Options for initiating an OAuth authorization request.
61#[derive(Debug)]
62pub struct AuthorizeOptions<S: BosStr = DefaultStr>
63where
64    S: AsRef<str>,
65{
66    /// Override the redirect URI registered in the client metadata.
67    pub redirect_uri: Option<Uri<String>>,
68    /// Scopes to request. Defaults to an empty list (server-defined defaults apply).
69    pub scopes: Scopes<S>,
70    /// Optional prompt hint for the authorization server's UI.
71    pub prompt: Option<AuthorizeOptionPrompt>,
72    /// Opaque client-provided state value, echoed back in the callback for CSRF protection.
73    pub state: Option<S>,
74}
75
76impl Default for AuthorizeOptions<DefaultStr> {
77    fn default() -> Self {
78        Self {
79            redirect_uri: None,
80            scopes: Scopes::empty(),
81            prompt: None,
82            state: None,
83        }
84    }
85}
86
87impl<S: BosStr + AsRef<str>> AuthorizeOptions<S> {
88    /// Set the `prompt` parameter sent to the authorization server.
89    pub fn with_prompt(mut self, prompt: AuthorizeOptionPrompt) -> Self {
90        self.prompt = Some(prompt);
91        self
92    }
93
94    /// Set a CSRF-protection `state` value to be echoed in the callback.
95    pub fn with_state(mut self, state: S) -> Self {
96        self.state = Some(state);
97        self
98    }
99
100    /// Override the redirect URI for this specific authorization request.
101    pub fn with_redirect_uri(mut self, redirect_uri: Uri<String>) -> Self {
102        self.redirect_uri = Some(redirect_uri);
103        self
104    }
105
106    /// Set the OAuth scopes to request.
107    pub fn with_scopes(mut self, scopes: Scopes<S>) -> Self {
108        self.scopes = scopes;
109        self
110    }
111}
112
113impl AuthorizeOptions<DefaultStr> {
114    /// Parse and set OAuth scopes from a space-separated scope string.
115    pub fn with_scope_str(mut self, scopes: impl AsRef<str>) -> Result<Self, ParseError> {
116        self.scopes = Scopes::new(SmolStr::new(scopes.as_ref()))?;
117        Ok(self)
118    }
119
120    /// Set OAuth scopes from one typed scope.
121    pub fn with_scope(self, scope: Scope<SmolStr>) -> Result<Self, ParseError> {
122        self.with_scope_iter([scope])
123    }
124
125    /// Set OAuth scopes from typed scope values.
126    pub fn with_scope_iter<I>(mut self, scopes: I) -> Result<Self, ParseError>
127    where
128        I: IntoIterator<Item = Scope<SmolStr>>,
129    {
130        self.scopes = Scopes::from_scopes(scopes)?;
131        Ok(self)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn authorize_options_accept_scope_string() {
141        let opts = AuthorizeOptions::default()
142            .with_scope_str("rpc:* atproto")
143            .unwrap();
144
145        assert_eq!(opts.scopes.to_normalized_string(), "atproto rpc:*");
146    }
147
148    #[test]
149    fn authorize_options_accept_typed_scopes() {
150        let opts = AuthorizeOptions::default()
151            .with_scope_iter([
152                Scope::atproto(),
153                Scope::rpc("app.bsky.feed.getTimeline").unwrap(),
154                Scope::repo_create("app.bsky.feed.post").unwrap(),
155            ])
156            .unwrap();
157
158        assert_eq!(
159            opts.scopes.to_normalized_string(),
160            "atproto repo:app.bsky.feed.post?action=create rpc:app.bsky.feed.getTimeline"
161        );
162    }
163
164    #[test]
165    fn authorize_options_accept_built_scopes() {
166        let scopes = Scopes::builder()
167            .atproto()
168            .transition_generic()
169            .rpc("app.bsky.feed.getTimeline")
170            .unwrap()
171            .build()
172            .unwrap();
173        let opts = AuthorizeOptions::default().with_scopes(scopes);
174
175        assert_eq!(
176            opts.scopes.to_normalized_string(),
177            "atproto rpc:app.bsky.feed.getTimeline transition:generic"
178        );
179    }
180}
181
182/// Query parameters delivered to the OAuth redirect URI after user authorization.
183#[derive(Debug, Deserialize)]
184#[serde(bound(deserialize = "S: serde::Deserialize<'de> + BosStr"))]
185pub struct CallbackParams<S: BosStr = DefaultStr> {
186    /// The authorization code issued by the authorization server.
187    pub code: S,
188    /// The `state` value originally sent in the authorization request, used to
189    /// verify the response belongs to this session.
190    pub state: Option<S>,
191    /// The `iss` (issuer) parameter, required by RFC 9207 to prevent mix-up attacks.
192    pub iss: Option<S>,
193}
194
195impl<S: BosStr + IntoStatic> IntoStatic for CallbackParams<S>
196where
197    S::Output: BosStr,
198{
199    type Output = CallbackParams<S::Output>;
200
201    fn into_static(self) -> Self::Output {
202        CallbackParams {
203            code: self.code.into_static(),
204            state: self.state.into_static(),
205            iss: self.iss.into_static(),
206        }
207    }
208}