use super::{
ACCESS_DENIED, AUTHORIZATION_PENDING, DEVICE_STATUS_APPROVED, DEVICE_STATUS_DENIED,
DEVICE_STATUS_PENDING, EXPIRED_DEVICE_CODE, INVALID_DEVICE_CODE, INVALID_DEVICE_CODE_STATUS,
POLLING_TOO_FREQUENTLY, USER_NOT_FOUND, device_error_response,
};
use crate::plugins::endpoint_context::EndpointContext;
use better_auth_core::{
AuthResult, AuthSchema, DeviceCode, DeviceCodeOwnership, UpdateDeviceCode,
store::DeviceCodeStore, wire::UserView,
};
use chrono::Utc;
use std::{future::Future, pin::Pin};
pub type DeviceRedemptionFuture<'a, T> = Pin<Box<dyn Future<Output = AuthResult<T>> + Send + 'a>>;
pub struct DeviceCodeRedemptionAuthorization<A> {
pub ownership: DeviceCodeOwnership,
pub context: A,
}
pub struct DeviceCodeRedemptionResult<A, R> {
pub claimed_device_code: DeviceCode,
pub authorization_context: A,
pub redemption_context: R,
pub user: UserView,
}
pub async fn redeem_device_code<S, A, R, Authorize, Prepare>(
endpoint: &EndpointContext<'_, S>,
code: &str,
authorize: Authorize,
prepare: Prepare,
) -> AuthResult<DeviceCodeRedemptionResult<A, R>>
where
S: AuthSchema,
A: Send + Sync,
R: Send,
Authorize: for<'a> FnOnce(
&'a DeviceCode,
&'a EndpointContext<'_, S>,
) -> DeviceRedemptionFuture<'a, DeviceCodeRedemptionAuthorization<A>>
+ Send,
Prepare: for<'a> FnOnce(
&'a DeviceCode,
&'a A,
&'a EndpointContext<'_, S>,
) -> DeviceRedemptionFuture<'a, R>
+ Send,
{
let store: &dyn DeviceCodeStore = match endpoint.transaction {
Some(transaction) => transaction,
None => endpoint.auth.database.as_ref(),
};
let Some(device_code) = store.get_device_code_by_device_code(code).await? else {
return Err(device_error_response(400, "invalid_grant", INVALID_DEVICE_CODE)?.into());
};
let authorization = authorize(&device_code, endpoint).await?;
if let (Some(last_polled_at), Some(polling_interval)) = (
device_code.last_polled_at.as_ref(),
device_code
.polling_interval
.filter(|interval| better_auth_core::FieldValue::Number(*interval).is_truthy()),
) {
let elapsed = Utc::now().timestamp_millis() as f64 - last_polled_at.milliseconds();
if elapsed < polling_interval {
return Err(device_error_response(400, "slow_down", POLLING_TOO_FREQUENTLY)?.into());
}
}
let _ = store
.update_device_code(
&device_code.id,
UpdateDeviceCode {
last_polled_at: Some(Some(Utc::now().into())),
..Default::default()
},
)
.await?;
if device_code.expires_at.milliseconds() < Utc::now().timestamp_millis() as f64 {
store.delete_device_code(&device_code.id).await?;
return Err(device_error_response(400, "expired_token", EXPIRED_DEVICE_CODE)?.into());
}
if device_code.status == DEVICE_STATUS_PENDING {
return Err(
device_error_response(400, "authorization_pending", AUTHORIZATION_PENDING)?.into(),
);
}
if device_code.status == DEVICE_STATUS_DENIED {
store.delete_device_code(&device_code.id).await?;
return Err(device_error_response(400, "access_denied", ACCESS_DENIED)?.into());
}
if device_code.status != DEVICE_STATUS_APPROVED {
return Err(device_error_response(500, "server_error", INVALID_DEVICE_CODE_STATUS)?.into());
}
let Some(user_id) = device_code.user_id.as_deref().filter(|id| !id.is_empty()) else {
return Err(device_error_response(500, "server_error", INVALID_DEVICE_CODE_STATUS)?.into());
};
let redemption_context = prepare(&device_code, &authorization.context, endpoint).await?;
let user = match endpoint.transaction {
Some(transaction) => transaction.get_user_by_id(user_id).await?,
None => endpoint.auth.database.get_user_by_id(user_id).await?,
};
let Some(user) = user else {
return Err(device_error_response(500, "server_error", USER_NOT_FOUND)?.into());
};
let user = endpoint.auth.internal_user_view(&user).await?;
let claimed = store
.consume_device_code(&device_code, &authorization.ownership)
.await?;
let Some(claimed_device_code) = claimed.filter(|row| {
row.user_id
.as_deref()
.is_some_and(|user_id| !user_id.is_empty())
}) else {
return Err(device_error_response(400, "invalid_grant", INVALID_DEVICE_CODE)?.into());
};
Ok(DeviceCodeRedemptionResult {
claimed_device_code,
authorization_context: authorization.context,
redemption_context,
user,
})
}