Skip to main content

alibi_plugins/oauth_provider/
refresh_format.rs

1//! Application-owned refresh presentation, after storage and before the wire prefix.
2use alibi_core::AuthResult;
3use async_trait::async_trait;
4use std::{future::Future, pin::Pin};
5
6/// A synchronous value or an asynchronous encryption callback result.
7///
8/// Pinned Better Auth 1.7.7 concatenates the result without awaiting it. A
9/// `Promise` therefore runs detached and presents as `[object Promise]`, not its
10/// resolved ciphertext. Use `Immediate` for usable encrypted credentials.
11/// Detached failures do not propagate to the response; this API does not emulate
12/// JavaScript's process-level unhandled-rejection policy.
13pub enum OAuthRefreshTokenEncryption {
14    Immediate(String),
15    Promise(Pin<Box<dyn Future<Output = AuthResult<String>> + Send + 'static>>),
16}
17
18/// Decoded storage preimage. In 1.7.7 consumers use the persisted session binding,
19/// not this optional callback-supplied session ID.
20#[derive(Debug, Clone)]
21pub struct OAuthDecodedRefreshToken {
22    pub token: String,
23    pub session_id: Option<String>,
24}
25
26/// Better Auth's `formatRefreshToken`. Both callbacks are required by its types;
27/// absence of the whole formatter preserves raw refresh credentials.
28#[async_trait]
29pub trait OAuthRefreshTokenFormatter: Send + Sync + std::fmt::Debug {
30    /// Invoked after refresh persistence (and rotation claim), before prefixing.
31    fn encrypt(
32        &self,
33        token: &str,
34        session_id: Option<&str>,
35    ) -> AuthResult<OAuthRefreshTokenEncryption>;
36
37    /// Invoked after removing the configured prefix; unlike encryption this is awaited.
38    async fn decrypt(&self, token: &str) -> AuthResult<OAuthDecodedRefreshToken>;
39}
40
41impl super::OAuthProviderPlugin {
42    pub(super) fn encode_refresh_token(
43        &self,
44        token: &str,
45        session_id: Option<&str>,
46    ) -> AuthResult<String> {
47        let formatted = match &self.config.refresh_token_formatter {
48            None => token.to_owned(),
49            Some(formatter) => match formatter.encrypt(token, session_id)? {
50                OAuthRefreshTokenEncryption::Immediate(value) => value,
51                OAuthRefreshTokenEncryption::Promise(future) => {
52                    // JS promises are eager; dropping a Rust future would suppress
53                    // application work. Neither its value nor rejection affects
54                    // the already-coerced wire value in pinned 1.7.7.
55                    drop(tokio::spawn(future));
56                    "[object Promise]".to_owned()
57                }
58            },
59        };
60        Ok(self.encode_token_prefix(&formatted, alibi_core::store::OAuthTokenKind::Refresh))
61    }
62
63    pub(super) async fn decode_refresh_token(
64        &self,
65        token: &str,
66    ) -> AuthResult<Option<OAuthDecodedRefreshToken>> {
67        let Some(token) =
68            self.decode_token_prefix(token, alibi_core::store::OAuthTokenKind::Refresh)
69        else {
70            return Ok(None);
71        };
72        Ok(Some(match &self.config.refresh_token_formatter {
73            Some(formatter) => formatter.decrypt(token).await?,
74            None => OAuthDecodedRefreshToken {
75                token: token.to_owned(),
76                session_id: None,
77            },
78        }))
79    }
80}