better_auth_api/plugins/device_authorization/
redemption.rs1use super::{
2 ACCESS_DENIED, AUTHORIZATION_PENDING, DEVICE_STATUS_APPROVED, DEVICE_STATUS_DENIED,
3 DEVICE_STATUS_PENDING, EXPIRED_DEVICE_CODE, INVALID_DEVICE_CODE, INVALID_DEVICE_CODE_STATUS,
4 POLLING_TOO_FREQUENTLY, USER_NOT_FOUND, device_error_response,
5};
6use crate::plugins::endpoint_context::EndpointContext;
7use better_auth_core::{
8 AuthResult, AuthSchema, DeviceCode, DeviceCodeOwnership, UpdateDeviceCode,
9 store::DeviceCodeStore, wire::UserView,
10};
11use chrono::Utc;
12use std::{future::Future, pin::Pin};
13
14pub type DeviceRedemptionFuture<'a, T> = Pin<Box<dyn Future<Output = AuthResult<T>> + Send + 'a>>;
16
17pub struct DeviceCodeRedemptionAuthorization<A> {
19 pub ownership: DeviceCodeOwnership,
21 pub context: A,
23}
24
25pub struct DeviceCodeRedemptionResult<A, R> {
27 pub claimed_device_code: DeviceCode,
29 pub authorization_context: A,
31 pub redemption_context: R,
33 pub user: UserView,
35}
36
37pub async fn redeem_device_code<S, A, R, Authorize, Prepare>(
44 endpoint: &EndpointContext<'_, S>,
45 code: &str,
46 authorize: Authorize,
47 prepare: Prepare,
48) -> AuthResult<DeviceCodeRedemptionResult<A, R>>
49where
50 S: AuthSchema,
51 A: Send + Sync,
52 R: Send,
53 Authorize: for<'a> FnOnce(
54 &'a DeviceCode,
55 &'a EndpointContext<'_, S>,
56 ) -> DeviceRedemptionFuture<'a, DeviceCodeRedemptionAuthorization<A>>
57 + Send,
58 Prepare: for<'a> FnOnce(
59 &'a DeviceCode,
60 &'a A,
61 &'a EndpointContext<'_, S>,
62 ) -> DeviceRedemptionFuture<'a, R>
63 + Send,
64{
65 let store: &dyn DeviceCodeStore = match endpoint.transaction {
66 Some(transaction) => transaction,
67 None => endpoint.auth.database.as_ref(),
68 };
69 let Some(device_code) = store.get_device_code_by_device_code(code).await? else {
70 return Err(device_error_response(400, "invalid_grant", INVALID_DEVICE_CODE)?.into());
71 };
72 let authorization = authorize(&device_code, endpoint).await?;
73 if let (Some(last_polled_at), Some(polling_interval)) = (
74 device_code.last_polled_at.as_ref(),
75 device_code
76 .polling_interval
77 .filter(|interval| better_auth_core::FieldValue::Number(*interval).is_truthy()),
78 ) {
79 let elapsed = Utc::now().timestamp_millis() as f64 - last_polled_at.milliseconds();
80 if elapsed < polling_interval {
81 return Err(device_error_response(400, "slow_down", POLLING_TOO_FREQUENTLY)?.into());
82 }
83 }
84 let _ = store
85 .update_device_code(
86 &device_code.id,
87 UpdateDeviceCode {
88 last_polled_at: Some(Some(Utc::now().into())),
89 ..Default::default()
90 },
91 )
92 .await?;
93 if device_code.expires_at.milliseconds() < Utc::now().timestamp_millis() as f64 {
94 store.delete_device_code(&device_code.id).await?;
95 return Err(device_error_response(400, "expired_token", EXPIRED_DEVICE_CODE)?.into());
96 }
97 if device_code.status == DEVICE_STATUS_PENDING {
98 return Err(
99 device_error_response(400, "authorization_pending", AUTHORIZATION_PENDING)?.into(),
100 );
101 }
102 if device_code.status == DEVICE_STATUS_DENIED {
103 store.delete_device_code(&device_code.id).await?;
104 return Err(device_error_response(400, "access_denied", ACCESS_DENIED)?.into());
105 }
106 if device_code.status != DEVICE_STATUS_APPROVED {
107 return Err(device_error_response(500, "server_error", INVALID_DEVICE_CODE_STATUS)?.into());
108 }
109 let Some(user_id) = device_code.user_id.as_deref().filter(|id| !id.is_empty()) else {
110 return Err(device_error_response(500, "server_error", INVALID_DEVICE_CODE_STATUS)?.into());
111 };
112 let redemption_context = prepare(&device_code, &authorization.context, endpoint).await?;
113 let user = match endpoint.transaction {
114 Some(transaction) => transaction.get_user_by_id(user_id).await?,
115 None => endpoint.auth.database.get_user_by_id(user_id).await?,
116 };
117 let Some(user) = user else {
118 return Err(device_error_response(500, "server_error", USER_NOT_FOUND)?.into());
119 };
120 let user = endpoint.auth.internal_user_view(&user).await?;
121 let claimed = store
122 .consume_device_code(&device_code, &authorization.ownership)
123 .await?;
124 let Some(claimed_device_code) = claimed.filter(|row| {
125 row.user_id
126 .as_deref()
127 .is_some_and(|user_id| !user_id.is_empty())
128 }) else {
129 return Err(device_error_response(400, "invalid_grant", INVALID_DEVICE_CODE)?.into());
130 };
131 Ok(DeviceCodeRedemptionResult {
132 claimed_device_code,
133 authorization_context: authorization.context,
134 redemption_context,
135 user,
136 })
137}