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#[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 NoDefaultConfiguration,
47 OrganizationIdRequired,
48 OrganizationPluginRequired,
49 UserNotMemberOfOrganization,
50 InsufficientApiKeyPermissions,
51 ServerOnlyProperty,
52 FailedToUpdateApiKey,
53 InvalidMetadataType,
54}
55
56impl ApiKeyErrorCode {
57 pub fn as_str(self) -> &'static str {
58 match self {
59 Self::InvalidApiKey => "INVALID_API_KEY",
60 Self::KeyDisabled => "KEY_DISABLED",
61 Self::KeyExpired => "KEY_EXPIRED",
62 Self::UsageExceeded => "USAGE_EXCEEDED",
63 Self::KeyNotFound => "KEY_NOT_FOUND",
64 Self::RateLimited => "RATE_LIMITED",
65 Self::UnauthorizedSession => "UNAUTHORIZED_SESSION",
66 Self::InvalidPrefixLength => "INVALID_PREFIX_LENGTH",
67 Self::InvalidNameLength => "INVALID_NAME_LENGTH",
68 Self::MetadataDisabled => "METADATA_DISABLED",
69 Self::NoValuesToUpdate => "NO_VALUES_TO_UPDATE",
70 Self::KeyDisabledExpiration => "KEY_DISABLED_EXPIRATION",
71 Self::ExpiresInTooSmall => "EXPIRES_IN_IS_TOO_SMALL",
72 Self::ExpiresInTooLarge => "EXPIRES_IN_IS_TOO_LARGE",
73 Self::InvalidRemaining => "INVALID_REMAINING",
74 Self::RefillAmountAndIntervalRequired => "REFILL_AMOUNT_AND_INTERVAL_REQUIRED",
75 Self::NameRequired => "NAME_REQUIRED",
76 Self::InvalidUserIdFromApiKey => "INVALID_USER_ID_FROM_API_KEY",
77 Self::NoDefaultConfiguration => "NO_DEFAULT_API_KEY_CONFIGURATION_FOUND",
78 Self::OrganizationIdRequired => "ORGANIZATION_ID_REQUIRED",
79 Self::OrganizationPluginRequired => "ORGANIZATION_PLUGIN_REQUIRED",
80 Self::UserNotMemberOfOrganization => "USER_NOT_MEMBER_OF_ORGANIZATION",
81 Self::InsufficientApiKeyPermissions => "INSUFFICIENT_API_KEY_PERMISSIONS",
82 Self::ServerOnlyProperty => "SERVER_ONLY_PROPERTY",
83 Self::FailedToUpdateApiKey => "FAILED_TO_UPDATE_API_KEY",
84 Self::InvalidMetadataType => "INVALID_METADATA_TYPE",
85 }
86 }
87
88 pub fn message(self) -> &'static str {
89 match self {
90 Self::InvalidApiKey => "Invalid API key.",
91 Self::KeyDisabled => "API Key is disabled",
92 Self::KeyExpired => "API Key has expired",
93 Self::UsageExceeded => "API Key has reached its usage limit",
94 Self::KeyNotFound => "API Key not found",
95 Self::RateLimited => "Rate limit exceeded.",
96 Self::UnauthorizedSession => "Unauthorized or invalid session",
97 Self::InvalidPrefixLength => "The prefix length is either too large or too small.",
98 Self::InvalidNameLength => "The name length is either too large or too small.",
99 Self::MetadataDisabled => "Metadata is disabled.",
100 Self::NoValuesToUpdate => "No values to update.",
101 Self::KeyDisabledExpiration => "Custom key expiration values are disabled.",
102 Self::ExpiresInTooSmall => {
103 "The expiresIn is smaller than the predefined minimum value."
104 }
105 Self::ExpiresInTooLarge => "The expiresIn is larger than the predefined maximum value.",
106 Self::InvalidRemaining => "The remaining count is either too large or too small.",
107 Self::RefillAmountAndIntervalRequired => {
108 "refillAmount and refillInterval must both be provided together"
109 }
110 Self::NameRequired => "API Key name is required.",
111 Self::InvalidUserIdFromApiKey => "The user id from the API key is invalid.",
112 Self::NoDefaultConfiguration => "No default api-key configuration found.",
113 Self::OrganizationIdRequired => {
114 "Organization ID is required for organization-owned API keys."
115 }
116 Self::OrganizationPluginRequired => {
117 "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
118 }
119 Self::UserNotMemberOfOrganization => {
120 "You are not a member of the organization that owns this API key."
121 }
122 Self::InsufficientApiKeyPermissions => {
123 "You do not have permission to perform this action on organization API keys."
124 }
125 Self::ServerOnlyProperty => {
126 "The property you're trying to set can only be set from the server auth instance only."
127 }
128 Self::FailedToUpdateApiKey => "Failed to update API key",
129 Self::InvalidMetadataType => "metadata must be an object or undefined",
130 }
131 }
132}
133
134pub(super) fn api_key_error(code: ApiKeyErrorCode) -> AuthError {
135 AuthError::bad_request(code.message())
136}
137
138pub(super) struct ApiKeyValidationError {
140 #[cfg_attr(
141 not(test),
142 expect(dead_code, reason = "read by the test module's verify_key helper")
143 )]
144 pub(super) code: ApiKeyErrorCode,
145 pub(super) message: String,
146}
147
148impl ApiKeyValidationError {
149 fn new(code: ApiKeyErrorCode) -> Self {
150 Self {
151 message: code.message().to_string(),
152 code,
153 }
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
163pub enum ApiKeyReferences {
164 #[default]
166 User,
167 Organization,
170}
171
172pub struct ApiKeyPlugin {
174 pub(super) configurations: Vec<ApiKeyConfig>,
177 last_expired_check: Mutex<Option<std::time::Instant>>,
179}
180
181impl ApiKeyPlugin {
182 pub fn configuration(mut self, config: ApiKeyConfig) -> Self {
187 self.configurations.push(config);
188 self
189 }
190
191 pub(super) fn resolve_configuration(
195 &self,
196 config_id: Option<&str>,
197 ) -> AuthResult<&ApiKeyConfig> {
198 if let Some(config_id) = config_id
199 && let Some(found) = self
200 .configurations
201 .iter()
202 .find(|config| config.config_id == config_id)
203 {
204 return Ok(found);
205 }
206
207 self.configurations
208 .iter()
209 .find(|config| is_default_config_id(&config.config_id))
210 .ok_or_else(|| api_key_error(ApiKeyErrorCode::NoDefaultConfiguration))
211 }
212
213 pub(super) fn default_configuration(&self) -> AuthResult<&ApiKeyConfig> {
215 self.resolve_configuration(None)
216 }
217}
218
219pub(super) fn is_default_config_id(config_id: &str) -> bool {
222 config_id.is_empty() || config_id == "default"
223}
224
225pub(super) fn config_id_matches(key_config_id: &str, expected: &str) -> bool {
227 if is_default_config_id(key_config_id) && is_default_config_id(expected) {
228 return true;
229 }
230 key_config_id == expected
231}
232
233#[derive(Debug, Clone)]
235pub struct ApiKeyConfig {
236 pub config_id: String,
239 pub references: ApiKeyReferences,
242
243 pub key_length: usize,
245 pub prefix: Option<String>,
246 pub default_remaining: Option<i64>,
247
248 pub api_key_header: String,
250
251 pub disable_key_hashing: bool,
253
254 pub starting_characters_length: usize,
256 pub store_starting_characters: bool,
257
258 pub max_prefix_length: usize,
260 pub min_prefix_length: usize,
261
262 pub max_name_length: usize,
264 pub min_name_length: usize,
265 pub require_name: bool,
266
267 pub enable_metadata: bool,
269
270 pub key_expiration: KeyExpirationConfig,
272
273 pub rate_limit: RateLimitDefaults,
275
276 pub enable_session_for_api_keys: bool,
278}
279
280#[derive(Debug, Clone)]
282pub struct KeyExpirationConfig {
283 pub default_expires_in: Option<i64>,
285 pub disable_custom_expires_time: bool,
287 pub max_expires_in: i64,
289 pub min_expires_in: i64,
291}
292
293impl Default for KeyExpirationConfig {
294 fn default() -> Self {
295 Self {
296 default_expires_in: None,
297 disable_custom_expires_time: false,
298 max_expires_in: 365,
299 min_expires_in: 1,
300 }
301 }
302}
303
304#[derive(Debug, Clone)]
306pub struct RateLimitDefaults {
307 pub enabled: bool,
308 pub time_window: i64,
310 pub max_requests: i64,
312}
313
314impl Default for RateLimitDefaults {
315 fn default() -> Self {
316 Self {
317 enabled: true,
318 time_window: 86_400_000, max_requests: 10,
320 }
321 }
322}
323
324impl Default for ApiKeyConfig {
325 fn default() -> Self {
326 Self {
327 config_id: "default".to_string(),
328 references: ApiKeyReferences::default(),
329 key_length: 64,
330 prefix: None,
331 default_remaining: None,
332 api_key_header: "x-api-key".to_string(),
333 disable_key_hashing: false,
334 starting_characters_length: 6,
335 store_starting_characters: true,
336 max_prefix_length: 32,
337 min_prefix_length: 1,
338 max_name_length: 32,
339 min_name_length: 1,
340 require_name: false,
341 enable_metadata: false,
342 key_expiration: KeyExpirationConfig::default(),
343 rate_limit: RateLimitDefaults::default(),
344 enable_session_for_api_keys: false,
345 }
346 }
347}
348
349#[bon::bon]
365impl ApiKeyPlugin {
366 #[builder]
367 pub fn new(
368 #[builder(default = "default".to_string())] config_id: String,
369 #[builder(default)] references: ApiKeyReferences,
370 #[builder(default = 64)] key_length: usize,
371 prefix: Option<String>,
372 default_remaining: Option<i64>,
373 #[builder(default = "x-api-key".to_string())] api_key_header: String,
374 #[builder(default = false)] disable_key_hashing: bool,
375 #[builder(default = 6)] starting_characters_length: usize,
376 #[builder(default = true)] store_starting_characters: bool,
377 #[builder(default = 32)] max_prefix_length: usize,
378 #[builder(default = 1)] min_prefix_length: usize,
379 #[builder(default = 32)] max_name_length: usize,
380 #[builder(default = 1)] min_name_length: usize,
381 #[builder(default = false)] require_name: bool,
382 #[builder(default = false)] enable_metadata: bool,
383 #[builder(default)] key_expiration: KeyExpirationConfig,
384 #[builder(default)] rate_limit: RateLimitDefaults,
385 #[builder(default = false)] enable_session_for_api_keys: bool,
386 ) -> Self {
387 Self {
388 configurations: vec![ApiKeyConfig {
389 config_id,
390 references,
391 key_length,
392 prefix,
393 default_remaining,
394 api_key_header,
395 disable_key_hashing,
396 starting_characters_length,
397 store_starting_characters,
398 max_prefix_length,
399 min_prefix_length,
400 max_name_length,
401 min_name_length,
402 require_name,
403 enable_metadata,
404 key_expiration,
405 rate_limit,
406 enable_session_for_api_keys,
407 }],
408 last_expired_check: Mutex::new(None),
409 }
410 }
411
412 pub fn with_config(config: ApiKeyConfig) -> Self {
413 Self {
414 configurations: vec![config],
415 last_expired_check: Mutex::new(None),
416 }
417 }
418
419 pub(super) fn generate_key(
422 config: &ApiKeyConfig,
423 custom_prefix: Option<&str>,
424 ) -> (String, String, String) {
425 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
427 let mut rng = rand::thread_rng();
428 let raw: String = (0..config.key_length)
429 .map(|_| {
430 ALPHABET
431 .choose(&mut rng)
432 .copied()
433 .map(char::from)
434 .unwrap_or('a')
435 })
436 .collect();
437
438 let prefix = custom_prefix.or(config.prefix.as_deref()).unwrap_or("");
439 let full_key = format!("{}{}", prefix, raw);
440
441 let start_len = config.starting_characters_length;
444 let start: String = full_key.chars().take(start_len).collect();
445
446 let hash = if config.disable_key_hashing {
447 full_key.clone()
448 } else {
449 Self::hash_key(&full_key)
450 };
451
452 (full_key, hash, start)
453 }
454
455 pub(super) fn hash_key(key: &str) -> String {
456 let mut hasher = Sha256::new();
457 hasher.update(key.as_bytes());
458 let digest = hasher.finalize();
459 URL_SAFE_NO_PAD.encode(digest)
460 }
461
462 pub(super) async fn maybe_delete_expired(
464 &self,
465 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
466 ) {
467 let should_run = {
468 let mut last = self
469 .last_expired_check
470 .lock()
471 .unwrap_or_else(|e| e.into_inner());
472 let now = std::time::Instant::now();
473 match *last {
474 Some(prev) if now.duration_since(prev).as_secs() < 10 => false,
475 _ => {
476 *last = Some(now);
477 true
478 }
479 }
480 };
481 if should_run {
482 let _ = ctx.database.delete_expired_api_keys().await;
483 }
484 }
485
486 pub(super) fn validate_prefix(config: &ApiKeyConfig, prefix: Option<&str>) -> AuthResult<()> {
489 if let Some(p) = prefix {
490 let len = p.len();
491 if len < config.min_prefix_length || len > config.max_prefix_length {
492 return Err(api_key_error(ApiKeyErrorCode::InvalidPrefixLength));
493 }
494 }
495 Ok(())
496 }
497
498 pub(super) fn validate_name(
504 config: &ApiKeyConfig,
505 name: Option<&str>,
506 is_create: bool,
507 ) -> AuthResult<()> {
508 if is_create && config.require_name && name.is_none() {
509 return Err(api_key_error(ApiKeyErrorCode::NameRequired));
510 }
511 if let Some(n) = name {
512 let len = n.len();
513 if len < config.min_name_length || len > config.max_name_length {
514 return Err(api_key_error(ApiKeyErrorCode::InvalidNameLength));
515 }
516 }
517 Ok(())
518 }
519
520 pub(super) fn validate_expires_in(
521 config: &ApiKeyConfig,
522 expires_in: Option<i64>,
523 ) -> AuthResult<Option<i64>> {
524 let cfg = &config.key_expiration;
525 if let Some(secs) = expires_in {
526 if cfg.disable_custom_expires_time {
527 return Err(api_key_error(ApiKeyErrorCode::KeyDisabledExpiration));
528 }
529 let days = secs as f64 / 86_400.0;
531 if days < cfg.min_expires_in as f64 {
532 return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooSmall));
533 }
534 if days > cfg.max_expires_in as f64 {
535 return Err(api_key_error(ApiKeyErrorCode::ExpiresInTooLarge));
536 }
537 Ok(Some(secs))
538 } else {
539 Ok(cfg.default_expires_in)
540 }
541 }
542
543 pub(super) fn validate_metadata(
544 config: &ApiKeyConfig,
545 metadata: &Option<serde_json::Value>,
546 ) -> AuthResult<()> {
547 if metadata.is_some() && !config.enable_metadata {
548 return Err(api_key_error(ApiKeyErrorCode::MetadataDisabled));
549 }
550 if let Some(v) = metadata
551 && !v.is_object()
552 && !v.is_null()
553 {
554 return Err(api_key_error(ApiKeyErrorCode::InvalidMetadataType));
555 }
556 Ok(())
557 }
558
559 pub(super) fn validate_refill(
560 refill_interval: Option<i64>,
561 refill_amount: Option<i64>,
562 ) -> AuthResult<()> {
563 match (refill_interval, refill_amount) {
564 (Some(_), None) | (None, Some(_)) => Err(api_key_error(
565 ApiKeyErrorCode::RefillAmountAndIntervalRequired,
566 )),
567 _ => Ok(()),
568 }
569 }
570
571 async fn handle_create(
576 &self,
577 req: &AuthRequest,
578 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
579 ) -> AuthResult<AuthResponse> {
580 let (user, _session) = ctx.require_session(req).await?;
581 let body: CreateKeyRequest = match better_auth_core::validate_request_body(req) {
582 Ok(v) => v,
583 Err(resp) => return Ok(resp),
584 };
585 let response = create_key_core(&body, user.id(), self, ctx).await?;
586 Ok(AuthResponse::json(200, &response)?)
587 }
588
589 async fn handle_get(
590 &self,
591 req: &AuthRequest,
592 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
593 ) -> AuthResult<AuthResponse> {
594 let (user, _session) = ctx.require_session(req).await?;
595 let id = req
596 .query
597 .get("id")
598 .ok_or_else(|| AuthError::bad_request("Query parameter 'id' is required"))?;
599 let config_id = req.query.get("configId").map(String::as_str);
600 let response = get_key_core(id, config_id, user.id(), self, ctx).await?;
601 Ok(AuthResponse::json(200, &response)?)
602 }
603
604 async fn handle_list(
605 &self,
606 req: &AuthRequest,
607 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
608 ) -> AuthResult<AuthResponse> {
609 let (user, _session) = ctx.require_session(req).await?;
610 let query = ListKeysQuery::from_request(req);
611 let response = list_keys_core(user.id(), &query, self, ctx).await?;
612 Ok(AuthResponse::json(200, &response)?)
613 }
614
615 async fn handle_update(
616 &self,
617 req: &AuthRequest,
618 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
619 ) -> AuthResult<AuthResponse> {
620 let (user, _session) = ctx.require_session(req).await?;
621 let body: UpdateKeyRequest = match better_auth_core::validate_request_body(req) {
622 Ok(v) => v,
623 Err(resp) => return Ok(resp),
624 };
625 let response = update_key_core(&body, user.id(), self, ctx).await?;
626 Ok(AuthResponse::json(200, &response)?)
627 }
628
629 async fn handle_delete(
630 &self,
631 req: &AuthRequest,
632 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
633 ) -> AuthResult<AuthResponse> {
634 let (user, _session) = ctx.require_session(req).await?;
635 let body: DeleteKeyRequest = match better_auth_core::validate_request_body(req) {
636 Ok(v) => v,
637 Err(resp) => return Ok(resp),
638 };
639 let response = delete_key_core(&body, user.id(), self, ctx).await?;
640 Ok(AuthResponse::json(200, &response)?)
641 }
642
643 pub(super) async fn validate_api_key(
651 &self,
652 ctx: &AuthContext<impl better_auth_core::AuthSchema>,
653 raw_key: &str,
654 required_permissions: Option<&serde_json::Value>,
655 ) -> Result<ApiKeyView, ApiKeyValidationError> {
656 let lookup_config = self
661 .default_configuration()
662 .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::NoDefaultConfiguration))?;
663 let hashed = if lookup_config.disable_key_hashing {
664 raw_key.to_string()
665 } else {
666 Self::hash_key(raw_key)
667 };
668
669 let api_key = ctx
671 .database
672 .get_api_key_by_hash(&hashed)
673 .await
674 .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey))?
675 .ok_or_else(|| ApiKeyValidationError::new(ApiKeyErrorCode::InvalidApiKey))?;
676
677 let config = self
678 .resolve_configuration(Some(&api_key.config_id))
679 .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::NoDefaultConfiguration))?;
680
681 if !api_key.enabled() {
683 return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyDisabled));
684 }
685
686 if let Some(expires_at_str) = api_key.expires_at()
688 && let Ok(expires_at) = chrono::DateTime::parse_from_rfc3339(expires_at_str)
689 && chrono::Utc::now() > expires_at
690 {
691 let _ = ctx.database.delete_api_key(&api_key.id()).await;
692 return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyExpired));
693 }
694
695 if let Some(required) = required_permissions {
697 let key_perms_str = api_key.permissions().unwrap_or("");
698 if key_perms_str.is_empty() {
699 return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyNotFound));
700 }
701 if !check_permissions(key_perms_str, required) {
702 return Err(ApiKeyValidationError::new(ApiKeyErrorCode::KeyNotFound));
703 }
704 }
705
706 let updated = match ctx
711 .database
712 .consume_api_key_usage(&api_key.id(), config.rate_limit.enabled)
713 .await
714 .map_err(|_| ApiKeyValidationError::new(ApiKeyErrorCode::FailedToUpdateApiKey))?
715 {
716 ConsumeApiKeyResult::Allowed(key) => *key,
717 ConsumeApiKeyResult::RateLimited => {
718 return Err(ApiKeyValidationError::new(ApiKeyErrorCode::RateLimited));
719 }
720 ConsumeApiKeyResult::UsageExhausted => {
721 return Err(ApiKeyValidationError::new(ApiKeyErrorCode::UsageExceeded));
722 }
723 };
724
725 self.maybe_delete_expired(ctx).await;
727
728 Ok(ApiKeyView::from(&updated))
729 }
730}
731
732better_auth_core::impl_auth_plugin! {
737 ApiKeyPlugin, "api-key";
738 routes {
739 post "/api-key/create" => handle_create, "api_key_create";
740 get "/api-key/get" => handle_get, "api_key_get";
741 post "/api-key/update" => handle_update, "api_key_update";
742 post "/api-key/delete" => handle_delete, "api_key_delete";
743 get "/api-key/list" => handle_list, "api_key_list";
744 }
745 extra {
746 async fn before_request(
747 &self,
748 req: &AuthRequest,
749 ctx: &AuthContext<S>,
750 ) -> AuthResult<Option<BeforeRequestAction>> {
751 let config = self.default_configuration()?;
752 if !config.enable_session_for_api_keys {
753 return Ok(None);
754 }
755
756 let raw_key = match req.headers.get(&config.api_key_header) {
758 Some(k) if !k.is_empty() => k.clone(),
759 _ => return Ok(None),
760 };
761
762 let view = self
764 .validate_api_key(ctx, &raw_key, None)
765 .await
766 .map_err(|e| AuthError::bad_request(e.message))?;
767
768 let user = ctx
770 .database
771 .get_user_by_id(&view.reference_id)
772 .await?
773 .ok_or_else(|| api_key_error(ApiKeyErrorCode::InvalidUserIdFromApiKey))?;
774
775 if req.path() == "/get-session" && req.method() == &better_auth_core::HttpMethod::Get {
781 let session_json = serde_json::json!({
782 "user": {
783 "id": user.id(),
784 "email": user.email(),
785 "name": user.name(),
786 },
787 "session": {
788 "id": view.id,
789 "token": raw_key,
790 "userId": view.reference_id,
791 }
792 });
793 return Ok(Some(BeforeRequestAction::Respond(AuthResponse::json(
794 200,
795 &session_json,
796 )?)));
797 }
798
799 Ok(Some(BeforeRequestAction::InjectSession {
801 user_id: view.reference_id,
802 session_token: raw_key,
803 }))
804 }
805 }
806}