Skip to main content

better_auth_api/plugins/api_key/
mod.rs

1use base64::Engine;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD;
3use rand::seq::SliceRandom;
4use sha2::{Digest, Sha256};
5use std::sync::Mutex;
6
7use better_auth_core::entity::{AuthApiKey as _, AuthUser};
8use better_auth_core::store::ConsumeApiKeyResult;
9use better_auth_core::{AuthContext, AuthError, AuthResult, BeforeRequestAction};
10use better_auth_core::{AuthRequest, AuthResponse};
11
12pub(super) mod handlers;
13pub(super) mod types;
14
15#[cfg(test)]
16mod tests;
17
18use handlers::*;
19use types::*;
20
21// ---------------------------------------------------------------------------
22// Error codes -- mirrors the TypeScript `API_KEY_ERROR_CODES`
23// ---------------------------------------------------------------------------
24
25/// Dedicated API Key error codes aligned with the TypeScript `API_KEY_ERROR_CODES`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ApiKeyErrorCode {
28    InvalidApiKey,
29    KeyDisabled,
30    KeyExpired,
31    UsageExceeded,
32    KeyNotFound,
33    RateLimited,
34    UnauthorizedSession,
35    InvalidPrefixLength,
36    InvalidNameLength,
37    MetadataDisabled,
38    NoValuesToUpdate,
39    KeyDisabledExpiration,
40    ExpiresInTooSmall,
41    ExpiresInTooLarge,
42    InvalidRemaining,
43    RefillAmountAndIntervalRequired,
44    NameRequired,
45    InvalidUserIdFromApiKey,
46    ServerOnlyProperty,
47    FailedToUpdateApiKey,
48    InvalidMetadataType,
49}
50
51impl ApiKeyErrorCode {
52    pub fn as_str(self) -> &'static str {
53        match self {
54            Self::InvalidApiKey => "INVALID_API_KEY",
55            Self::KeyDisabled => "KEY_DISABLED",
56            Self::KeyExpired => "KEY_EXPIRED",
57            Self::UsageExceeded => "USAGE_EXCEEDED",
58            Self::KeyNotFound => "KEY_NOT_FOUND",
59            Self::RateLimited => "RATE_LIMITED",
60            Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
61            Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
62            Self::InvalidNameLength => "INVALID_NAME_LENGTH",
63            Self::MetadataDisabled => "METADATA_DISABLED",
64            Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
65            Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
66            Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
67            Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
68            Self::InvalidRemaining => "INVALID_REMAINING",
69            Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
70            Self::NameRequired => "NAME_REQUIRED",
71            Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
72            Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
73            Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
74            Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
75        }
76    }
77
78    pub fn message(self) -> &'static str {
79        match self {
80            Self::InvalidApiKey => "Invalid API key.",
81            Self::KeyDisabled => "API Key is disabled",
82            Self::KeyExpired => "API Key has expired",
83            Self::UsageExceeded => "API Key has reached its usage limit",
84            Self::KeyNotFound => "API Key not found",
85            Self::RateLimited => "Rate limit exceeded.",
86            Self::UnauthorizedSession => "Unauthorized or invalid session",
87            Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
88            Self::InvalidNameLength => "The name length is either too large or too small.",
89            Self::MetadataDisabled => "Metadata is disabled.",
90            Self::NoValuesToUpdate => "No values to update.",
91            Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
92            Self::ExpiresInTooSmall => {
93                "The expiresIn is smaller than the predefined minimum value."
94            }
95            Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
96            Self::InvalidRemaining => "The remaining count is either too large or too small.",
97            Self::RefillAmountAndIntervalRequired => {
98                "refillAmount and refillInterval must both be provided together"
99            }
100            Self::NameRequired => "API Key name is required.",
101            Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
102            Self::ServerOnlyProperty => {
103                "The property you're trying to set can only be set from the server auth instance only."
104            }
105            Self::FailedToUpdateApiKey => "Failed to update API key",
106            Self::InvalidMetadataType => "metadata must be an object or undefined",
107        }
108    }
109}
110
111fn api_key_error(code: ApiKeyErrorCode) -> AuthError {
112    AuthError::bad_request(code.message())
113}
114
115/// Structured error returned by `validate_api_key`.
116pub(super) struct ApiKeyValidationError {
117    #[cfg_attr(
118        not(test),
119        expect(dead_code, reason = "read by the test module's verify_key helper")
120    )]
121    pub(super) code: ApiKeyErrorCode,
122    pub(super) message: String,
123}
124
125impl ApiKeyValidationError {
126    fn new(code: ApiKeyErrorCode) -> Self {
127        Self {
128            message: code.message().to_string(),
129            code,
130        }
131    }
132}
133
134// ---------------------------------------------------------------------------
135// Configuration
136// ---------------------------------------------------------------------------
137
138/// API Key management plugin.
139pub struct ApiKeyPlugin {
140    pub(super) config: ApiKeyConfig,
141    /// Throttle for `delete_expired_api_keys` -- stores the last check instant.
142    last_expired_check: Mutex<Option<std::time::Instant>>,
143}
144
145/// Configuration for the API Key plugin, aligned with the TypeScript `ApiKeyOptions`.
146#[derive(Debug, Clone)]
147pub struct ApiKeyConfig {
148    // -- key generation --
149    pub key_length: usize,
150    pub prefix: Option<String>,
151    pub default_remaining: Option<i64>,
152
153    // -- header --
154    pub api_key_header: String,
155
156    // -- hashing --
157    pub disable_key_hashing: bool,
158
159    // -- starting characters --
160    pub starting_characters_length: usize,
161    pub store_starting_characters: bool,
162
163    // -- prefix length validation --
164    pub max_prefix_length: usize,
165    pub min_prefix_length: usize,
166
167    // -- name validation --
168    pub max_name_length: usize,
169    pub min_name_length: usize,
170    pub require_name: bool,
171
172    // -- metadata --
173    pub enable_metadata: bool,
174
175    // -- key expiration --
176    pub key_expiration: KeyExpirationConfig,
177
178    // -- rate limit defaults --
179    pub rate_limit: RateLimitDefaults,
180
181    // -- session emulation --
182    pub enable_session_for_api_keys: bool,
183}
184
185/// Key expiration constraints.
186#[derive(Debug, Clone)]
187pub struct KeyExpirationConfig {
188    /// Default `expiresIn` (in milliseconds) when none is provided. `None` = no default.
189    pub default_expires_in: Option<i64>,
190    /// If true, clients cannot set a custom `expiresIn`.
191    pub disable_custom_expires_time: bool,
192    /// Maximum `expiresIn` in **days**.
193    pub max_expires_in: i64,
194    /// Minimum `expiresIn` in **days**.
195    pub min_expires_in: i64,
196}
197
198impl Default for KeyExpirationConfig {
199    fn default() -> Self {
200        Self {
201            default_expires_in: None,
202            disable_custom_expires_time: false,
203            max_expires_in: 365,
204            min_expires_in: 1,
205        }
206    }
207}
208
209/// Global rate-limit defaults applied to newly-created keys.
210#[derive(Debug, Clone)]
211pub struct RateLimitDefaults {
212    pub enabled: bool,
213    /// Default time window in milliseconds.
214    pub time_window: i64,
215    /// Default max requests per window.
216    pub max_requests: i64,
217}
218
219impl Default for RateLimitDefaults {
220    fn default() -> Self {
221        Self {
222            enabled: true,
223            time_window: 86_400_000, // 24 hours
224            max_requests: 10,
225        }
226    }
227}
228
229impl Default for ApiKeyConfig {
230    fn default() -> Self {
231        Self {
232            key_length: 64,
233            prefix: None,
234            default_remaining: None,
235            api_key_header: "x-api-key".to_string(),
236            disable_key_hashing: false,
237            starting_characters_length: 6,
238            store_starting_characters: true,
239            max_prefix_length: 32,
240            min_prefix_length: 1,
241            max_name_length: 32,
242            min_name_length: 1,
243            require_name: false,
244            enable_metadata: false,
245            key_expiration: KeyExpirationConfig::default(),
246            rate_limit: RateLimitDefaults::default(),
247            enable_session_for_api_keys: false,
248        }
249    }
250}
251
252// ---------------------------------------------------------------------------
253// Plugin implementation
254// ---------------------------------------------------------------------------
255
256/// Builder for [`ApiKeyPlugin`] powered by the `bon` crate.
257///
258/// Usage:
259/// ```ignore
260/// let plugin = ApiKeyPlugin::builder()
261///     .key_length(48)
262///     .prefix("ba_".to_string())
263///     .enable_metadata(true)
264///     .rate_limit(RateLimitDefaults { enabled: true, time_window: 60_000, max_requests: 5 })
265///     .build();
266/// ```
267#[bon::bon]
268impl ApiKeyPlugin {
269    #[builder]
270    pub fn new(
271        #[builder(default = 64)] key_length: usize,
272        prefix: Option<String>,
273        default_remaining: Option<i64>,
274        #[builder(default = "x-api-key".to_string())] api_key_header: String,
275        #[builder(default = false)] disable_key_hashing: bool,
276        #[builder(default = 6)] starting_characters_length: usize,
277        #[builder(default = true)] store_starting_characters: bool,
278        #[builder(default = 32)] max_prefix_length: usize,
279        #[builder(default = 1)] min_prefix_length: usize,
280        #[builder(default = 32)] max_name_length: usize,
281        #[builder(default = 1)] min_name_length: usize,
282        #[builder(default = false)] require_name: bool,
283        #[builder(default = false)] enable_metadata: bool,
284        #[builder(default)] key_expiration: KeyExpirationConfig,
285        #[builder(default)] rate_limit: RateLimitDefaults,
286        #[builder(default = false)] enable_session_for_api_keys: bool,
287    ) -> Self {
288        Self {
289            config: ApiKeyConfig {
290                key_length,
291                prefix,
292                default_remaining,
293                api_key_header,
294                disable_key_hashing,
295                starting_characters_length,
296                store_starting_characters,
297                max_prefix_length,
298                min_prefix_length,
299                max_name_length,
300                min_name_length,
301                require_name,
302                enable_metadata,
303                key_expiration,
304                rate_limit,
305                enable_session_for_api_keys,
306            },
307            last_expired_check: Mutex::new(None),
308        }
309    }
310
311    pub fn with_config(config: ApiKeyConfig) -> Self {
312        Self {
313            config,
314            last_expired_check: Mutex::new(None),
315        }
316    }
317
318    // -- internal helpers --
319
320    pub(super) fn generate_key(&self, custom_prefix: Option<&str>) -> (String, String, String) {
321        // Match TS: generateRandomString(length, "a-z", "A-Z") — alpha only
322        const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
323        let mut rng = rand::thread_rng();
324        let raw: String = (0..self.config.key_length)
325            .map(|_| {
326                ALPHABET
327                    .choose(&mut rng)
328                    .copied()
329                    .map(char::from)
330                    .unwrap_or('a')
331            })
332            .collect();
333
334        let prefix = custom_prefix
335            .or(self.config.prefix.as_deref())
336            .unwrap_or("");
337        let full_key = format!("{}{}", prefix, raw);
338
339        // TS computes start from the full key (including prefix):
340        //   start = key.substring(0, charactersLength)
341        let start_len = self.config.starting_characters_length;
342        let start: String = full_key.chars().take(start_len).collect();
343
344        let hash = if self.config.disable_key_hashing {
345            full_key.clone()
346        } else {
347            Self::hash_key(&full_key)
348        };
349
350        (full_key, hash, start)
351    }
352
353    fn hash_key(key: &str) -> String {
354        let mut hasher = Sha256::new();
355        hasher.update(key.as_bytes());
356        let digest = hasher.finalize();
357        URL_SAFE_NO_PAD.encode(digest)
358    }
359
360    /// Throttled cleanup -- at most once per 10 seconds.
361    pub(super) async fn maybe_delete_expired(
362        &self,
363        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
364    ) {
365        let should_run = {
366            let mut last = self
367                .last_expired_check
368                .lock()
369                .unwrap_or_else(|e| e.into_inner());
370            let now = std::time::Instant::now();
371            match *last {
372                Some(prev) if now.duration_since(prev).as_secs() < 10 => false,
373                _ => {
374                    *last = Some(now);
375                    true
376                }
377            }
378        };
379        if should_run {
380            let _ = ctx.database.delete_expired_api_keys().await;
381        }
382    }
383
384    // -- Validation helpers --
385
386    pub(super) fn validate_prefix(&self, prefix: Option<&str>) -> AuthResult<()> {
387        if let Some(p) = prefix {
388            let len = p.len();
389            if len < self.config.min_prefix_length || len > self.config.max_prefix_length {
390                return Err(api_key_error(ApiKeyErrorCode::InvalidPrefixLength));
391            }
392        }
393        Ok(())
394    }
395
396    /// Validate the `name` field.
397    ///
398    /// When `is_create` is true, `require_name` is enforced (name must be
399    /// present).  On updates `require_name` is **not** enforced -- the
400    /// caller may be updating unrelated fields without resending the name.
401    pub(super) fn validate_name(&self, name: Option<&str>, is_create: bool) -> AuthResult<()> {
402        if is_create && self.config.require_name && name.is_none() {
403            return Err(api_key_error(ApiKeyErrorCode::NameRequired));
404        }
405        if let Some(n) = name {
406            let len = n.len();
407            if len < self.config.min_name_length || len > self.config.max_name_length {
408                return Err(api_key_error(ApiKeyErrorCode::InvalidNameLength));
409            }
410        }
411        Ok(())
412    }
413
414    pub(super) fn validate_expires_in(&self, expires_in: Option<i64>) -> AuthResult<Option<i64>> {
415        let cfg = &self.config.key_expiration;
416        if let Some(secs) = expires_in {
417            if cfg.disable_custom_expires_time {
418                return Err(api_key_error(ApiKeyErrorCode::KeyDisabledExpiration));
419            }
420            // expiresIn is in seconds; min/max are in days
421            let days = secs as f64 / 86_400.0;
422            if days < cfg.min_expires_in as f64 {
423                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooSmall));
424            }
425            if days > cfg.max_expires_in as f64 {
426                return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooLarge));
427            }
428            Ok(Some(secs))
429        } else {
430            Ok(cfg.default_expires_in)
431        }
432    }
433
434    pub(super) fn validate_metadata(&self, metadata: &Option<serde_json::Value>) -> AuthResult<()> {
435        if metadata.is_some() && !self.config.enable_metadata {
436            return Err(api_key_error(ApiKeyErrorCode::MetadataDisabled));
437        }
438        if let Some(v) = metadata
439            && !v.is_object()
440            && !v.is_null()
441        {
442            return Err(api_key_error(ApiKeyErrorCode::InvalidMetadataType));
443        }
444        Ok(())
445    }
446
447    pub(super) fn validate_refill(
448        refill_interval: Option<i64>,
449        refill_amount: Option<i64>,
450    ) -> AuthResult<()> {
451        match (refill_interval, refill_amount) {
452            (Some(_), None) | (None, Some(_)) => Err(api_key_error(
453                ApiKeyErrorCode::RefillAmountAndIntervalRequired,
454            )),
455            _ => Ok(()),
456        }
457    }
458
459    // -----------------------------------------------------------------------
460    // Route handlers
461    // -----------------------------------------------------------------------
462
463    async fn handle_create(
464        &self,
465        req: &AuthRequest,
466        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
467    ) -> AuthResult<AuthResponse> {
468        let (user, _session) = ctx.require_session(req).await?;
469        let body: CreateKeyRequest = match better_auth_core::validate_request_body(req) {
470            Ok(v) => v,
471            Err(resp) => return Ok(resp),
472        };
473        let response = create_key_core(&body, user.id(), self, ctx).await?;
474        Ok(AuthResponse::json(200, &response)?)
475    }
476
477    async fn handle_get(
478        &self,
479        req: &AuthRequest,
480        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
481    ) -> AuthResult<AuthResponse> {
482        let (user, _session) = ctx.require_session(req).await?;
483        let id = req
484            .query
485            .get("id")
486            .ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
487        let response = get_key_core(id, user.id(), self, ctx).await?;
488        Ok(AuthResponse::json(200, &response)?)
489    }
490
491    async fn handle_list(
492        &self,
493        req: &AuthRequest,
494        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
495    ) -> AuthResult<AuthResponse> {
496        let (user, _session) = ctx.require_session(req).await?;
497        let response = list_keys_core(user.id(), self, ctx).await?;
498        Ok(AuthResponse::json(200, &response)?)
499    }
500
501    async fn handle_update(
502        &self,
503        req: &AuthRequest,
504        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
505    ) -> AuthResult<AuthResponse> {
506        let (user, _session) = ctx.require_session(req).await?;
507        let body: UpdateKeyRequest = match better_auth_core::validate_request_body(req) {
508            Ok(v) => v,
509            Err(resp) => return Ok(resp),
510        };
511        let response = update_key_core(&body, user.id(), self, ctx).await?;
512        Ok(AuthResponse::json(200, &response)?)
513    }
514
515    async fn handle_delete(
516        &self,
517        req: &AuthRequest,
518        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
519    ) -> AuthResult<AuthResponse> {
520        let (user, _session) = ctx.require_session(req).await?;
521        let body: DeleteKeyRequest = match better_auth_core::validate_request_body(req) {
522            Ok(v) => v,
523            Err(resp) => return Ok(resp),
524        };
525        let response = delete_key_core(&body, user.id(), self, ctx).await?;
526        Ok(AuthResponse::json(200, &response)?)
527    }
528
529    /// Core validation logic used by `before_request` and tests.
530    ///
531    /// Validation chain: exists -> disabled -> expired -> permissions ->
532    /// remaining/refill -> rate limit.
533    ///
534    /// Returns `Ok(ApiKeyView)` on success, or `Err(ApiKeyValidationError)` with
535    /// a structured error code.
536    pub(super) async fn validate_api_key(
537        &self,
538        ctx: &AuthContext<impl better_auth_core::AuthSchema>,
539        raw_key: &str,
540        required_permissions: Option<&serde_json::Value>,
541    ) -> Result<ApiKeyView, ApiKeyValidationError> {
542        // Hash the key (or use as-is if hashing is disabled)
543        let hashed = if self.config.disable_key_hashing {
544            raw_key.to_string()
545        } else {
546            Self::hash_key(raw_key)
547        };
548
549        // Look up by hash
550        let api_key = ctx
551            .database
552            .get_api_key_by_hash(&hashed)
553            .await
554            .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey))?
555            .ok_or_else(|| ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey))?;
556
557        // 1. Disabled?
558        if !api_key.enabled() {
559            return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyDisabled));
560        }
561
562        // 2. Expired?
563        if let Some(expires_at_str) = api_key.expires_at()
564            && let Ok(expires_at) = chrono::DateTime::parse_from_rfc3339(expires_at_str)
565            && chrono::Utc::now() > expires_at
566        {
567            let _ = ctx.database.delete_api_key(&api_key.id()).await;
568            return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyExpired));
569        }
570
571        // 3. Permissions check
572        if let Some(required) = required_permissions {
573            let key_perms_str = api_key.permissions().unwrap_or("");
574            if key_perms_str.is_empty() {
575                return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyNotFound));
576            }
577            if !check_permissions(key_perms_str, required) {
578                return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyNotFound));
579            }
580        }
581
582        // 4. Atomically consume one use: decrement remaining (with refill),
583        // increment rate-limit counter, update timestamps. All counter
584        // mutations happen inside a transaction against the locked row to
585        // prevent concurrent requests from corrupting counters.
586        let updated = match ctx
587            .database
588            .consume_api_key_usage(&api_key.id(), self.config.rate_limit.enabled)
589            .await
590            .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::FailedToUpdateApiKey))?
591        {
592            ConsumeApiKeyResult::Allowed(key) => *key,
593            ConsumeApiKeyResult::RateLimited => {
594                return Err(ApiKeyValidationError::new(ApiKeyErrorCode::RateLimited));
595            }
596            ConsumeApiKeyResult::UsageExhausted => {
597                return Err(ApiKeyValidationError::new(ApiKeyErrorCode::UsageExceeded));
598            }
599        };
600
601        // Throttled cleanup
602        self.maybe_delete_expired(ctx).await;
603
604        Ok(ApiKeyView::from(&updated))
605    }
606}
607
608// ---------------------------------------------------------------------------
609// AuthPlugin trait implementation
610// ---------------------------------------------------------------------------
611
612better_auth_core::impl_auth_plugin! {
613    ApiKeyPlugin, "api-key";
614    routes {
615        post "/api-key/create"                    => handle_create,             "api_key_create";
616        get  "/api-key/get"                       => handle_get,                "api_key_get";
617        post "/api-key/update"                    => handle_update,             "api_key_update";
618        post "/api-key/delete"                    => handle_delete,             "api_key_delete";
619        get  "/api-key/list"                      => handle_list,               "api_key_list";
620    }
621    extra {
622        async fn before_request(
623            &self,
624            req: &AuthRequest,
625            ctx: &AuthContext<S>,
626        ) -> AuthResult<Option<BeforeRequestAction>> {
627            if !self.config.enable_session_for_api_keys {
628                return Ok(None);
629            }
630
631            // Check for API key in the configured header
632            let raw_key = match req.headers.get(&self.config.api_key_header) {
633                Some(k) if !k.is_empty() => k.clone(),
634                _ => return Ok(None),
635            };
636
637            // Validate the key (reuses the full verify logic)
638            let view = self
639                .validate_api_key(ctx, &raw_key, None)
640                .await
641                .map_err(|e| AuthError::bad_request(e.message))?;
642
643            // Look up the user
644            let user = ctx
645                .database
646                .get_user_by_id(&view.user_id)
647                .await?
648                .ok_or_else(|| api_key_error(ApiKeyErrorCode::InvalidUserIdFromApiKey))?;
649
650            // Build a virtual session response for `/get-session`
651            if req.path() == "/get-session" {
652                let session_json = serde_json::json!({
653                    "user": {
654                        "id": user.id(),
655                        "email": user.email(),
656                        "name": user.name(),
657                    },
658                    "session": {
659                        "id": view.id,
660                        "token": raw_key,
661                        "userId": view.user_id,
662                    }
663                });
664                return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
665                    200,
666                    &session_json,
667                )?)));
668            }
669
670            // For all other routes, inject the session
671            Ok(Some(BeforeRequestAction::InjectSession {
672                user_id: view.user_id,
673                session_token: raw_key,
674            }))
675        }
676    }
677}