openauth_plugins/anonymous/
hooks.rs1use http::header;
2use openauth_core::api::{ApiRequest, ApiResponse};
3use openauth_core::context::request_state::current_new_session;
4use openauth_core::context::AuthContext;
5use openauth_core::error::OpenAuthError;
6use openauth_core::plugin::{AuthPlugin, PluginAfterHookAction};
7use serde::Serialize;
8
9use super::model::{self, AnonymousSession, LinkedSession};
10use super::options::AnonymousOptions;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13pub struct AnonymousLinkAccount {
14 pub anonymous_user: AnonymousSession,
15 pub new_user: LinkedSession,
16}
17
18pub fn attach_link_hooks(mut plugin: AuthPlugin, options: AnonymousOptions) -> AuthPlugin {
19 for path in [
20 "/sign-in*",
21 "/sign-up*",
22 "/callback*",
23 "/oauth2/callback*",
24 "/magic-link/verify*",
25 "/email-otp/verify-email*",
26 "/one-tap/callback*",
27 "/passkey/verify-authentication*",
28 "/phone-number/verify*",
29 ] {
30 let options = options.clone();
31 plugin = plugin.with_async_after_hook(path, move |context, request, response| {
32 let options = options.clone();
33 Box::pin(async move { link_after_hook(context, request, response, options).await })
34 });
35 }
36 plugin
37}
38
39async fn link_after_hook(
40 context: &AuthContext,
41 request: &ApiRequest,
42 response: ApiResponse,
43 options: AnonymousOptions,
44) -> Result<PluginAfterHookAction, OpenAuthError> {
45 let adapter = context.adapter().ok_or_else(|| {
46 OpenAuthError::Adapter("anonymous plugin requires a database adapter".to_owned())
47 })?;
48 let cookie_header = request
49 .headers()
50 .get(header::COOKIE)
51 .and_then(|value| value.to_str().ok())
52 .unwrap_or_default()
53 .to_owned();
54 let Some(anonymous_user) = model::current_anonymous_session(
55 adapter.as_ref(),
56 context,
57 options.storage_field_name(),
58 cookie_header,
59 )
60 .await?
61 else {
62 return Ok(PluginAfterHookAction::Continue(response));
63 };
64 if !anonymous_user.user.is_anonymous {
65 return Ok(PluginAfterHookAction::Continue(response));
66 }
67
68 let new_user = if let Some(new_user) =
69 linked_session_from_request_state(adapter.as_ref(), &options).await?
70 {
71 new_user
72 } else {
73 let Some(new_session_token) = new_session_token(context, &response)? else {
74 return Ok(PluginAfterHookAction::Continue(response));
75 };
76 let Some(new_user) = model::linked_session_from_token(
77 adapter.as_ref(),
78 options.storage_field_name(),
79 &new_session_token,
80 )
81 .await?
82 else {
83 return Ok(PluginAfterHookAction::Continue(response));
84 };
85 new_user
86 };
87
88 finish_link(
89 response,
90 adapter.as_ref(),
91 options,
92 anonymous_user,
93 new_user,
94 )
95 .await
96}
97
98async fn linked_session_from_request_state(
99 adapter: &dyn openauth_core::db::DbAdapter,
100 options: &AnonymousOptions,
101) -> Result<Option<LinkedSession>, OpenAuthError> {
102 let Some(new_session) = current_new_session_or_none()? else {
103 return Ok(None);
104 };
105 let Some(user) =
106 model::find_anonymous_user(adapter, options.storage_field_name(), &new_session.user.id)
107 .await?
108 else {
109 return Ok(None);
110 };
111 Ok(Some(LinkedSession {
112 session: new_session.session,
113 user,
114 }))
115}
116
117fn current_new_session_or_none(
118) -> Result<Option<openauth_core::context::request_state::NewSession>, OpenAuthError> {
119 match current_new_session() {
120 Ok(session) => Ok(session),
121 Err(OpenAuthError::RequestStateMissing) => Ok(None),
122 Err(error) => Err(error),
123 }
124}
125
126async fn finish_link(
127 response: ApiResponse,
128 adapter: &dyn openauth_core::db::DbAdapter,
129 options: AnonymousOptions,
130 anonymous_user: AnonymousSession,
131 new_user: LinkedSession,
132) -> Result<PluginAfterHookAction, OpenAuthError> {
133 if let Some(callback) = &options.on_link_account {
134 callback(AnonymousLinkAccount {
135 anonymous_user: anonymous_user.clone(),
136 new_user: new_user.clone(),
137 })
138 .await?;
139 }
140
141 if options.disable_delete_anonymous_user
142 || new_user.user.id == anonymous_user.user.id
143 || new_user.user.is_anonymous
144 {
145 return Ok(PluginAfterHookAction::Continue(response));
146 }
147
148 model::delete_anonymous_user_records(adapter, &anonymous_user.user.id).await?;
149
150 Ok(PluginAfterHookAction::Continue(response))
151}
152
153fn new_session_token(
154 context: &AuthContext,
155 response: &ApiResponse,
156) -> Result<Option<String>, OpenAuthError> {
157 for value in response.headers().get_all(header::SET_COOKIE) {
158 let Ok(cookie) = value.to_str() else {
159 continue;
160 };
161 let Some(raw_value) = cookie_value(cookie, &context.auth_cookies.session_token.name) else {
162 continue;
163 };
164 if let Some(token) = model::verified_cookie_value(context, raw_value)? {
165 return Ok(Some(token));
166 }
167 }
168 Ok(None)
169}
170
171fn cookie_value<'a>(set_cookie: &'a str, name: &str) -> Option<&'a str> {
172 let (cookie_name, rest) = set_cookie.split_once('=')?;
173 if cookie_name.trim() != name {
174 return None;
175 }
176 Some(rest.split_once(';').map_or(rest, |(value, _)| value))
177}