A better Rust ATProto crate
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 6.4 kB View raw
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}