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