1mod account;
2mod endpoint;
3pub use account::{OAuthAccountApi, OAuthAccountSelection};
4pub(crate) use endpoint::start_social_flow_response;
5pub use types::{
6 AccessTokenResponse as OAuthAccessToken, AccountInfoAccount as OAuthAccountIdentity,
7 AccountInfoResponse as OAuthAccountProfile,
8};
9mod client_assertion;
10pub(crate) use client_assertion::ALGORITHMS as PRIVATE_KEY_JWT_SIGNING_ALGORITHMS;
11pub use client_assertion::OAuthPrivateKeyJwtOptions;
12
13pub(crate) mod logout;
14
15mod account_cookie;
16
17pub mod encryption;
18
19pub(crate) mod handlers;
20
21pub(crate) mod id_token;
22
23mod providers;
24
25pub(crate) mod state;
26
27mod types;
28use alibi_core::AuthResult;
29use alibi_core::{AuthContext, AuthPlugin, AuthRoute};
30use alibi_core::{AuthRequest, AuthResponse, HttpMethod};
31use async_trait::async_trait;
32pub use endpoint::OAuthRefreshResult;
33pub(crate) use handlers::{
34 OAuthProcessPolicy, OAuthSignInError, create_account_cookie_headers, process_oauth_sign_in,
35};
36pub use handlers::{oauth_callback_path, oauth_disable_sign_up_option, resolve_oauth_account_key};
37pub use id_token::{
38 HttpOAuthJwksSource, OAuthIdTokenClaimsVerifier, OAuthIdTokenConfig, OAuthJwksSelection,
39 OAuthJwksSource, OAuthNonceComparison,
40};
41pub use providers::{
42 AppleOptions, AtlassianOptions, CloudflareOptions, CognitoOptions, DropboxAccessType,
43 DropboxOptions, FacebookOptions, FigmaOptions, GenericOAuthConfig, GenericOAuthError,
44 GenericOAuthMetadata, GenericOAuthResolved, HuggingFaceOptions, KakaoOptions, KickOptions,
45 LineOptions, LinearOptions, LinkedInOptions, MicrosoftOptions, MicrosoftProfilePhotoSize,
46 NaverOptions, NotionOptions, OAuthAccountKey, OAuthAccountKeyContext, OAuthAccountKeyResolver,
47 OAuthAccountSubject, OAuthAuthorizationCodeCallback, OAuthAuthorizationCodeContext,
48 OAuthAuthorizationCodeHandler, OAuthAuthorizationPolicy, OAuthCallbackUserName,
49 OAuthCallbackUserPayload, OAuthClientAssertion, OAuthClientAssertionContext,
50 OAuthClientAssertionGetter, OAuthConfig, OAuthEndSessionConfig, OAuthIdTokenVerifier,
51 OAuthProfileMapper, OAuthProvider, OAuthRefreshContext, OAuthRefreshTokenHandler,
52 OAuthRefreshTokenParams, OAuthRefreshTokenParamsResolver, OAuthScopeEncoding, OAuthScopeOrder,
53 OAuthTokenEndpointAuth, OAuthTokenGrant, OAuthTokenSet, OAuthUserInfo, OAuthUserInfoHandler,
54 OAuthUserInfoRequest, OAuthUserInfoResponse, PayPalEnvironment, PayPalOptions, PaybinOptions,
55 PolarOptions, RailwayOptions, RedditOptions, RobloxOptions, SalesforceEnvironment,
56 SalesforceOptions, SlackOptions, SpotifyOptions, TikTokOptions, TwitchOptions, TwitterOptions,
57 VercelOptions, VkOptions, WeChatLanguage, WeChatOptions, ZoomOptions,
58};
59pub(crate) use state::{
60 CapturedOAuthServerContext, OAuthServerContext, RecoveredOAuthServerContext,
61};
62pub use types::{
63 LinkSocialRequest as OAuthLinkSocialRequest, OAuthCallbackInput, OAuthIdTokenRequest,
64 SocialSignInRequest as OAuthSocialSignInRequest,
65 SocialSignInResponse as OAuthSocialSignInResponse,
66};
67
68pub struct OAuthPlugin {
69 config: OAuthConfig,
70}
71
72impl OAuthPlugin {
73 #[must_use]
74 pub fn new() -> Self {
75 Self {
76 config: OAuthConfig::default(),
77 }
78 }
79
80 #[must_use]
81 pub const fn with_config(config: OAuthConfig) -> Self {
82 Self { config }
83 }
84
85 #[must_use]
86 pub fn add_provider(mut self, name: &str, provider: OAuthProvider) -> Self {
87 _ = self.config.providers.insert(name.to_owned(), provider);
88 self
89 }
90}
91
92impl Default for OAuthPlugin {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98#[async_trait]
99impl<S: alibi_core::AuthSchema> AuthPlugin<S> for OAuthPlugin {
100 route_openapi_metadata!(S);
101
102 fn name(&self) -> &'static str {
103 "oauth"
104 }
105
106 fn server_endpoints(&self) -> Vec<alibi_core::endpoint::EndpointDefinition> {
107 endpoint::definitions()
108 }
109 fn validate_endpoint(
110 &self,
111 call: &alibi_core::endpoint::EndpointCall,
112 _: &AuthContext<S>,
113 ) -> AuthResult<alibi_core::endpoint::EndpointInput> {
114 endpoint::validate(call)
115 }
116 async fn on_endpoint(
117 &self,
118 call: &alibi_core::endpoint::EndpointCall,
119 ctx: &AuthContext<S>,
120 ) -> AuthResult<alibi_core::endpoint::EndpointResponse> {
121 if call.operation_id() == "handleOAuthCallback" {
122 return self.call_callback_endpoint(call, ctx).await;
123 }
124 if call.operation_id() == "linkSocialAccount" {
125 return self.call_link_endpoint(call, ctx).await;
126 }
127 if call.operation_id() == "socialSignIn" {
128 return self.call_social_endpoint(call, ctx).await;
129 }
130 self.call_account_endpoint(call, ctx).await
131 }
132
133 fn endpoint_hooks(&self) -> Vec<&dyn alibi_core::endpoint::EndpointHook<S>> {
134 vec![self]
135 }
136 fn routes(&self) -> Vec<AuthRoute> {
137 vec![
138 AuthRoute::post("/sign-in/social", "social_sign_in"),
139 AuthRoute::get("/callback/{provider}", "oauth_callback")
140 .with_context_path("/callback/:id"),
141 AuthRoute::post("/callback/{provider}", "oauth_callback_post")
142 .with_context_path("/callback/:id"),
143 AuthRoute::post("/link-social", "link_social"),
144 AuthRoute::post("/get-access-token", "get_access_token"),
145 AuthRoute::post("/refresh-token", "refresh_token"),
146 AuthRoute::get("/account-info", "account_info"),
147 ]
148 }
149
150 async fn on_init(&self, ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
151 ctx.extensions.insert(self.config.clone());
152 Ok(())
153 }
154
155 fn allowed_media_types(&self, route: &AuthRoute) -> Vec<&'static str> {
156 if route.path == "/callback/{provider}" {
157 vec!["application/x-www-form-urlencoded", "application/json"]
158 } else {
159 vec!["application/json"]
160 }
161 }
162
163 async fn on_request(
164 &self,
165 req: &AuthRequest,
166 ctx: &AuthContext<S>,
167 ) -> AuthResult<Option<AuthResponse>> {
168 match (req.method(), req.path()) {
169 (HttpMethod::Post, "/sign-in/social") => Ok(Some(
170 handlers::handle_social_sign_in(&self.config, req, ctx).await?,
171 )),
172 (HttpMethod::Get | HttpMethod::Post, path) if path_matches_callback(path) => {
173 let provider = extract_provider_from_callback(path);
174 Ok(Some(
175 handlers::handle_callback(&self.config, &provider, req, ctx).await?,
176 ))
177 }
178 (HttpMethod::Post, "/link-social") => Ok(Some(
179 handlers::handle_link_social(&self.config, req, ctx).await?,
180 )),
181 (HttpMethod::Post, "/get-access-token") => Ok(Some(
182 account::handle_get_access_token(&self.config, req, ctx).await?,
183 )),
184 (HttpMethod::Post, "/refresh-token") => Ok(Some(
185 account::handle_refresh_token(&self.config, req, ctx).await?,
186 )),
187 (HttpMethod::Get, "/account-info") => Ok(Some(
188 account::handle_account_info(&self.config, req, ctx).await?,
189 )),
190 _ => Ok(None),
191 }
192 }
193
194 async fn after_request(
195 &self,
196 req: &AuthRequest,
197 ctx: &AuthContext<S>,
198 mut response: AuthResponse,
199 ) -> AuthResult<AuthResponse> {
200 for header in Self::cache_account_headers(req, ctx, &response.headers)? {
201 response.headers.append("set-cookie", header);
202 }
203 Ok(response)
204 }
205}
206
207impl std::fmt::Debug for OAuthPlugin {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 f.debug_struct("OAuthPlugin").finish_non_exhaustive()
210 }
211}
212
213fn path_matches_callback(path: &str) -> bool {
215 let path_without_query = path.split('?').next().unwrap_or(path);
216 path_without_query
217 .strip_prefix("/callback/")
218 .is_some_and(|provider| !provider.is_empty() && !provider.contains('/'))
219}
220
221fn extract_provider_from_callback(path: &str) -> String {
223 let path_without_query = path.split('?').next().unwrap_or(path);
224 path_without_query
225 .strip_prefix("/callback/")
226 .unwrap_or_default()
227 .to_owned()
228}
229
230impl OAuthPlugin {
231 fn cache_account_headers<S: alibi_core::AuthSchema>(
232 req: &impl alibi_core::session::SessionRequest,
233 ctx: &AuthContext<S>,
234 headers: &alibi_core::Headers,
235 ) -> AuthResult<Vec<String>> {
236 let account_name =
239 alibi_core::utils::cookie_utils::related_cookie_name(&ctx.config, "account_data");
240 let cache_name =
241 alibi_core::utils::cookie_utils::related_cookie_name(&ctx.config, "session_data");
242 let account_chunks = format!("{account_name}.");
243 let cache_chunks = format!("{cache_name}.");
244 let pending: Vec<_> = headers
245 .get_all("set-cookie")
246 .filter_map(|raw| cookie::Cookie::parse(raw.clone()).ok())
247 .collect();
248 if !ctx.config.account.store_account_cookie
249 || pending.iter().any(|cookie| {
250 cookie.name() == account_name || cookie.name().starts_with(&account_chunks)
251 })
252 || !pending.iter().any(|cookie| {
253 (cookie.name() == cache_name || cookie.name().starts_with(&cache_chunks))
254 && !cookie.value().is_empty()
255 && cookie.max_age().is_none_or(|age| age.whole_seconds() != 0)
256 })
257 {
258 return Ok(Vec::new());
259 }
260 let Some((user, _)) = alibi_core::session::cookie_cache::runtime::published_session(req)
261 .or_else(|| {
262 req.endpoint_call()
263 .and_then(alibi_core::endpoint::EndpointCall::session_hook_snapshot)
264 })
265 .or_else(|| {
266 req.physical_request()
267 .and_then(AuthRequest::session_hook_snapshot)
268 })
269 else {
270 return Ok(Vec::new());
271 };
272 let Ok(Some(account)) = handlers::decode_account_cookie(req, &ctx.config) else {
273 return Ok(Vec::new());
274 };
275 let headers = if account.user_id == user.id {
276 handlers::create_account_cookie_headers(&ctx.config, &account, req)?
277 } else {
278 alibi_core::session::cookie_cache::runtime::chunked_cookie_headers(
279 &account_name,
280 "",
281 Some(0.0),
282 &ctx.config,
283 req.session_headers(),
284 true,
285 )?
286 };
287 Ok(headers)
288 }
289}
290
291#[async_trait]
292impl<S: alibi_core::AuthSchema> alibi_core::endpoint::EndpointHook<S> for OAuthPlugin {
293 async fn after(
294 &self,
295 call: &alibi_core::endpoint::EndpointCall,
296 ctx: &AuthContext<S>,
297 mut response: alibi_core::endpoint::EndpointResponse,
298 ) -> AuthResult<alibi_core::endpoint::EndpointResponse> {
299 for header in Self::cache_account_headers(call, ctx, response.headers())? {
300 response = response.with_header("set-cookie", header);
301 }
302 Ok(response)
303 }
304}