Skip to main content

better_auth_api/plugins/device_authorization/
grant.rs

1use super::{
2    DeviceAuthorizationPlugin, DeviceAuthorizationRequest, INVALID_CLIENT_ID, device_error_response,
3};
4use crate::plugins::endpoint_context::{EndpointContext, WithCallbacks};
5use better_auth_core::{
6    AuthContext, AuthError, AuthInitContext, AuthPlugin, AuthResult, AuthSchema, DeviceCode,
7    store::schema::EntityRole, user_fields::UserConfig,
8};
9use serde_json::{Map, Value};
10use std::{future::Future, pin::Pin, sync::Arc};
11
12/// A grant callback that borrows its request or device code and active endpoint.
13pub type DeviceGrantFuture<'a, T> = Pin<Box<dyn Future<Output = AuthResult<T>> + Send + 'a>>;
14
15type Authorize<S> = dyn for<'a> Fn(
16        &'a DeviceAuthorizationRequest,
17        &'a EndpointContext<'_, S>,
18    ) -> DeviceGrantFuture<'a, Option<DeviceGrantAuthorization>>
19    + Send
20    + Sync;
21type SessionPolicy<S> = dyn for<'a> Fn(&'a DeviceCode, &'a EndpointContext<'_, S>) -> DeviceGrantFuture<'a, ()>
22    + Send
23    + Sync;
24type VerificationContext =
25    dyn Fn(&DeviceCode) -> AuthResult<Option<Map<String, Value>>> + Send + Sync;
26
27#[derive(Clone, Default)]
28pub(super) struct GrantMetadata {
29    pub(super) request_error_codes: Vec<String>,
30    pub(super) request_responses: Map<String, Value>,
31    pub(super) verification_properties: Map<String, Value>,
32}
33
34/// The client binding and declared fields produced by grant request authorization.
35pub struct DeviceGrantAuthorization {
36    /// Client identifier that owns the issued code.
37    pub client_id: String,
38    /// Logical stored fields; native code, owner, status, and scope remain authoritative.
39    pub additional_fields: better_auth_core::FieldMap,
40}
41
42/// Request authorization and session redemption policy for one Device plugin.
43pub struct DeviceGrant<S: AuthSchema> {
44    stored_fields: UserConfig,
45    metadata: GrantMetadata,
46    authorize: Arc<Authorize<S>>,
47    session_policy: Arc<SessionPolicy<S>>,
48    verification: Option<Arc<VerificationContext>>,
49}
50
51impl<S: AuthSchema> DeviceGrant<S> {
52    /// Require both request authorization and a session redemption policy.
53    ///
54    /// Authorization returning `None` uses the configured client validator.
55    /// The session policy runs after the stored client check and before redemption state changes.
56    pub fn new<A, R>(authorize_request: A, assert_session_redemption: R) -> Self
57    where
58        A: for<'a> Fn(
59                &'a DeviceAuthorizationRequest,
60                &'a EndpointContext<'_, S>,
61            ) -> DeviceGrantFuture<'a, Option<DeviceGrantAuthorization>>
62            + Send
63            + Sync
64            + 'static,
65        R: for<'a> Fn(&'a DeviceCode, &'a EndpointContext<'_, S>) -> DeviceGrantFuture<'a, ()>
66            + Send
67            + Sync
68            + 'static,
69    {
70        Self {
71            stored_fields: UserConfig::default(),
72            metadata: GrantMetadata::default(),
73            authorize: Arc::new(authorize_request),
74            session_policy: Arc::new(assert_session_redemption),
75            verification: None,
76        }
77    }
78
79    /// Register declared grant fields through the existing adapter field policies.
80    pub fn stored_fields(mut self, fields: UserConfig) -> Self {
81        self.stored_fields = fields;
82        self
83    }
84
85    /// Append documented request error codes in order, retaining duplicate values.
86    pub fn request_error_codes(mut self, codes: Vec<String>) -> Self {
87        self.metadata.request_error_codes = codes;
88        self
89    }
90
91    /// Document additional request responses. The fixed 200, 400, and 500 responses take precedence.
92    pub fn request_openapi_responses(mut self, responses: Map<String, Value>) -> Self {
93        self.metadata.request_responses = responses;
94        self
95    }
96
97    /// Document verification display properties. Initialization rejects native property names.
98    pub fn verification_openapi_properties(mut self, properties: Map<String, Value>) -> Self {
99        self.metadata.verification_properties = properties;
100        self
101    }
102
103    /// Add display data only when the current session owns the verification request.
104    /// Native response fields retain precedence, including their omission.
105    pub fn verification_context(
106        mut self,
107        callback: impl Fn(&DeviceCode) -> AuthResult<Option<Map<String, Value>>> + Send + Sync + 'static,
108    ) -> Self {
109        self.verification = Some(Arc::new(callback));
110        self
111    }
112
113    pub(super) async fn assert_session_redemption(
114        &self,
115        device_code: &DeviceCode,
116        endpoint: &EndpointContext<'_, S>,
117    ) -> AuthResult<()> {
118        (self.session_policy)(device_code, endpoint).await
119    }
120
121    pub(super) fn verification_context_for(
122        &self,
123        device_code: &DeviceCode,
124    ) -> AuthResult<Map<String, Value>> {
125        let mut fields = match &self.verification {
126            Some(callback) => callback(device_code)?.unwrap_or_default(),
127            None => Map::new(),
128        };
129        // Upstream assigns every native property after the grant object, including undefined values.
130        for name in ["user_code", "status", "client_id", "scope"] {
131            let _ = fields.remove(name);
132        }
133        Ok(fields)
134    }
135}
136
137impl DeviceAuthorizationPlugin {
138    /// Attach a grant after configuring ordinary plugin options and request fields.
139    pub fn grant<S: AuthSchema>(mut self, grant: DeviceGrant<S>) -> impl AuthPlugin<S> {
140        self.config.grant_fields = Some(grant.stored_fields.clone());
141        self.config.grant_metadata = grant.metadata.clone();
142        WithCallbacks {
143            plugin: self,
144            callbacks: Arc::new(grant),
145        }
146    }
147
148    pub(super) fn configured_grant<S: AuthSchema>(
149        &self,
150        context: &AuthContext<S>,
151    ) -> AuthResult<Option<Arc<DeviceGrant<S>>>> {
152        if self.config.grant_fields.is_none() {
153            return Ok(None);
154        }
155        context
156            .extensions
157            .get::<Arc<DeviceGrant<S>>>()
158            .cloned()
159            .map(Some)
160            .ok_or_else(|| AuthError::internal("Device grant callbacks are not registered"))
161    }
162
163    pub(super) async fn authorize_request<S: AuthSchema>(
164        &self,
165        request: &DeviceAuthorizationRequest,
166        endpoint: &EndpointContext<'_, S>,
167    ) -> AuthResult<DeviceGrantAuthorization> {
168        let grant = self
169            .configured_grant(endpoint.auth)?
170            .ok_or_else(|| AuthError::internal("Device grant callbacks are not registered"))?;
171        let authorization = (grant.authorize)(request, endpoint).await?;
172        let authorization = match authorization {
173            Some(authorization) => authorization,
174            None => {
175                let Some(client_id) = request
176                    .client_id
177                    .as_deref()
178                    .filter(|value| !value.is_empty())
179                else {
180                    return Err(missing_client()?);
181                };
182                if self.config.validate_client.is_none()
183                    || !self.validate_client_id(client_id).await?
184                {
185                    return Err(
186                        device_error_response(400, "invalid_client", INVALID_CLIENT_ID)?.into(),
187                    );
188                }
189                DeviceGrantAuthorization {
190                    client_id: client_id.to_owned(),
191                    additional_fields: Default::default(),
192                }
193            }
194        };
195        if authorization.client_id.is_empty() {
196            return Err(missing_client()?);
197        }
198        Ok(authorization)
199    }
200}
201
202fn missing_client() -> AuthResult<AuthError> {
203    Ok(device_error_response(400, "invalid_request", "client_id is required")?.into())
204}
205
206pub(super) fn register_fields<S: AuthSchema>(
207    context: &mut AuthInitContext<S>,
208    fields: &UserConfig,
209) -> AuthResult<()> {
210    let conflicts: Vec<_> = fields
211        .additional_fields
212        .as_ref()
213        .into_iter()
214        .flat_map(|fields| fields.keys())
215        .filter(|name| {
216            matches!(
217                name.as_str(),
218                "deviceCode"
219                    | "userCode"
220                    | "userId"
221                    | "expiresAt"
222                    | "status"
223                    | "lastPolledAt"
224                    | "pollingInterval"
225                    | "clientId"
226                    | "scope"
227            )
228        })
229        .map(String::as_str)
230        .collect();
231    if !conflicts.is_empty() {
232        return Err(AuthError::config(format!(
233            "Device authorization grant fields must be additional and cannot redefine deviceCode fields: {}",
234            conflicts.join(", "),
235        )));
236    }
237    context.register_model_fields(EntityRole::DeviceCode, fields.clone())
238}