better_auth_api/plugins/device_authorization/
grant.rs1use 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
12pub 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
34pub struct DeviceGrantAuthorization {
36 pub client_id: String,
38 pub additional_fields: better_auth_core::FieldMap,
40}
41
42pub 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 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 pub fn stored_fields(mut self, fields: UserConfig) -> Self {
81 self.stored_fields = fields;
82 self
83 }
84
85 pub fn request_error_codes(mut self, codes: Vec<String>) -> Self {
87 self.metadata.request_error_codes = codes;
88 self
89 }
90
91 pub fn request_openapi_responses(mut self, responses: Map<String, Value>) -> Self {
93 self.metadata.request_responses = responses;
94 self
95 }
96
97 pub fn verification_openapi_properties(mut self, properties: Map<String, Value>) -> Self {
99 self.metadata.verification_properties = properties;
100 self
101 }
102
103 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 for name in ["user_code", "status", "client_id", "scope"] {
131 let _ = fields.remove(name);
132 }
133 Ok(fields)
134 }
135}
136
137impl DeviceAuthorizationPlugin {
138 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}