1use super::types::DeviceCodeRequest;
3use super::{
4 ACCESS_DENIED, AUTHORIZATION_PENDING, DEVICE_STATUS_APPROVED, DEVICE_STATUS_DENIED,
5 DEVICE_STATUS_PENDING, DeviceAuthorizationPlugin, EXPIRED_DEVICE_CODE, INVALID_DEVICE_CODE,
6 INVALID_DEVICE_CODE_STATUS, POLLING_TOO_FREQUENTLY, USER_NOT_FOUND, device_error_response,
7 no_store, validate_device_media,
8};
9use crate::helpers::callback_failure;
10use alibi_core::{
11 AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, AuthSchema, DeviceCode,
12 UpdateDeviceCode,
13};
14use async_trait::async_trait;
15use chrono::Utc;
16use serde_json::{Map, Value};
17use std::sync::Arc;
18
19#[derive(Debug)]
20pub enum DeviceGrantFailure {
21 OAuth {
22 status: u16,
23 error: String,
24 description: String,
25 },
26 Application(AuthError),
27}
28impl From<AuthError> for DeviceGrantFailure {
29 fn from(error: AuthError) -> Self {
30 Self::Application(error)
31 }
32}
33impl DeviceGrantFailure {
34 pub fn oauth(status: u16, error: impl Into<String>, description: impl Into<String>) -> Self {
35 Self::OAuth {
36 status,
37 error: error.into(),
38 description: description.into(),
39 }
40 }
41 pub fn into_response(self) -> AuthResult<AuthResponse> {
42 match self {
43 Self::OAuth {
44 status,
45 error,
46 description,
47 } => device_error_response(status, &error, &description),
48 Self::Application(error) => Err(callback_failure(error)),
49 }
50 }
51}
52#[derive(Debug, Clone)]
53pub struct DeviceGrantRecord {
54 pub device_code: DeviceCode,
55 pub fields: Map<String, Value>,
56}
57#[derive(Debug, Clone)]
58pub struct DeviceGrantAuthorization {
59 pub client_id: String,
60 pub user_id: Option<String>,
61 pub fields: Map<String, Value>,
62}
63
64#[async_trait]
66pub trait DeviceAuthorizationGrant: Send + Sync {
67 fn request_schema_fields(&self) -> Map<String, Value> {
69 Map::new()
70 }
71 fn on_request_validation_error(&self, issues: &[String]) -> DeviceGrantFailure {
72 DeviceGrantFailure::oauth(400, "invalid_request", issues.join("; "))
73 }
74 async fn authorize_request(
75 &self,
76 request: &Map<String, Value>,
77 original: &AuthRequest,
78 ) -> Result<DeviceGrantAuthorization, DeviceGrantFailure>;
79 async fn assert_session_redemption(
80 &self,
81 _record: &DeviceGrantRecord,
82 ) -> Result<(), DeviceGrantFailure> {
83 Ok(())
84 }
85 async fn verification_context(
86 &self,
87 _record: &DeviceGrantRecord,
88 ) -> AuthResult<Map<String, Value>> {
89 Ok(Map::new())
90 }
91 fn device_code_schema_fields(&self) -> Vec<alibi_core::OpenApiField> {
92 Vec::new()
93 }
94 fn request_error_codes(&self) -> Vec<String> {
95 Vec::new()
96 }
97 fn request_openapi_responses(&self) -> Map<String, Value> {
98 Map::new()
99 }
100 fn verification_openapi_properties(&self) -> Map<String, Value> {
101 Map::new()
102 }
103}
104#[derive(Debug, Clone)]
105pub struct DeviceRedemptionAuthorization {
106 pub ownership: Map<String, Value>,
107 pub context: Value,
108}
109#[async_trait]
111pub trait DeviceRedemptionPolicy: Send + Sync {
112 async fn authorize(
113 &self,
114 record: &DeviceGrantRecord,
115 ) -> AuthResult<DeviceRedemptionAuthorization>;
116 async fn prepare(&self, record: &DeviceGrantRecord, authorization: &Value)
117 -> AuthResult<Value>;
118}
119pub struct DeviceRedemptionResult<S: AuthSchema> {
120 pub claimed_device_code: DeviceGrantRecord,
121 pub user: S::User,
122 pub authorization_context: Value,
123 pub redemption_context: Value,
124}
125
126pub async fn redeem_device_code<S: AuthSchema>(
128 ctx: &AuthContext<S>,
129 code: &str,
130 policy: &dyn DeviceRedemptionPolicy,
131) -> Result<DeviceRedemptionResult<S>, DeviceGrantFailure> {
132 let row = ctx
133 .database
134 .get_device_code_by_device_code(code)
135 .await?
136 .ok_or_else(|| DeviceGrantFailure::oauth(400, "invalid_grant", INVALID_DEVICE_CODE))?;
137 let record = DeviceGrantRecord {
138 fields: ctx.database.device_code_fields(&row.id).await?,
139 device_code: row,
140 };
141 let authorization = policy.authorize(&record).await?;
142 let row = &record.device_code;
143 let now = Utc::now();
144 if let (Some(last), Some(interval)) = (row.last_polled_at, row.polling_interval)
145 && interval != 0
146 && now.signed_duration_since(last).num_milliseconds() < interval
147 {
148 return Err(DeviceGrantFailure::oauth(
149 400,
150 "slow_down",
151 POLLING_TOO_FREQUENTLY,
152 ));
153 }
154 _ = ctx
155 .database
156 .update_device_code(
157 &row.id,
158 UpdateDeviceCode {
159 last_polled_at: Some(Some(now)),
160 ..Default::default()
161 },
162 )
163 .await?;
164 if row.expires_at < now {
165 ctx.database.delete_device_code(&row.id).await?;
166 return Err(DeviceGrantFailure::oauth(
167 400,
168 "expired_token",
169 EXPIRED_DEVICE_CODE,
170 ));
171 }
172 match row.status.as_str() {
173 DEVICE_STATUS_PENDING => {
174 return Err(DeviceGrantFailure::oauth(
175 400,
176 "authorization_pending",
177 AUTHORIZATION_PENDING,
178 ));
179 }
180 DEVICE_STATUS_DENIED => {
181 ctx.database.delete_device_code(&row.id).await?;
182 return Err(DeviceGrantFailure::oauth(
183 400,
184 "access_denied",
185 ACCESS_DENIED,
186 ));
187 }
188 DEVICE_STATUS_APPROVED if row.user_id.is_some() => {}
189 _ => {
190 return Err(DeviceGrantFailure::oauth(
191 500,
192 "server_error",
193 INVALID_DEVICE_CODE_STATUS,
194 ));
195 }
196 }
197 let redemption_context = policy.prepare(&record, &authorization.context).await?;
198 let user = ctx
199 .database
200 .get_user_by_id(
201 row.user_id
202 .as_deref()
203 .ok_or_else(|| DeviceGrantFailure::oauth(500, "server_error", USER_NOT_FOUND))?,
204 )
205 .await?
206 .ok_or_else(|| DeviceGrantFailure::oauth(500, "server_error", USER_NOT_FOUND))?;
207 let claimed = ctx
208 .database
209 .consume_device_code(&row.id, DEVICE_STATUS_APPROVED, &authorization.ownership)
210 .await?
211 .ok_or_else(|| DeviceGrantFailure::oauth(400, "invalid_grant", INVALID_DEVICE_CODE))?;
212 Ok(DeviceRedemptionResult {
213 claimed_device_code: DeviceGrantRecord {
214 device_code: claimed,
215 fields: record.fields,
216 },
217 user,
218 authorization_context: authorization.context,
219 redemption_context,
220 })
221}
222
223impl DeviceAuthorizationPlugin {
224 #[must_use]
225 pub fn grant(mut self, grant: impl DeviceAuthorizationGrant + 'static) -> Self {
226 self.config.grant = Some(Arc::new(grant));
227 self
228 }
229 pub(super) async fn handle_application_issuance(
230 &self,
231 req: &AuthRequest,
232 ctx: &AuthContext<impl AuthSchema>,
233 grant: &dyn DeviceAuthorizationGrant,
234 ) -> AuthResult<AuthResponse> {
235 let is_form = match validate_device_media(req, true) {
236 Ok(form) => form,
237 Err(response) => return Ok(response),
238 };
239 let body: Value = if is_form {
240 Value::Object(
241 url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
242 .map(|(key, value)| (key.into_owned(), Value::String(value.into_owned())))
243 .collect(),
244 )
245 } else {
246 req.body_as_json()?
247 };
248 let mut request = body.as_object().cloned().unwrap_or_default();
249 let fields = grant.request_schema_fields();
250 let mut issues = Vec::new();
251 for (name, schema) in &fields {
252 let value = request.get(name).and_then(Value::as_str);
253 let minimum = schema.get("minLength").and_then(Value::as_u64).unwrap_or(0);
254 if value.is_none_or(|value| {
255 value.encode_utf16().count() < usize::try_from(minimum).unwrap_or(usize::MAX)
256 }) {
257 issues.push(name.clone());
258 }
259 }
260 for field in ["scope", "client_id", "user_id"] {
261 if request.get(field).is_some_and(|value| !value.is_string()) {
262 issues.push(field.into());
263 }
264 }
265 if !issues.is_empty() {
266 return grant.on_request_validation_error(&issues).into_response();
267 }
268 request.retain(|name, _| {
269 ["scope", "client_id", "user_id"].contains(&name.as_str()) || fields.contains_key(name)
270 });
271 let authorized = match grant.authorize_request(&request, req).await {
272 Ok(value) => value,
273 Err(error) => {
274 return error.into_response().map(no_store);
275 }
276 };
277 self.issue_device_code_with_fields(
278 DeviceCodeRequest {
279 client_id: authorized.client_id,
280 user_id: authorized.user_id,
281 scope: request
282 .get("scope")
283 .and_then(Value::as_str)
284 .map(str::to_owned),
285 },
286 authorized.fields,
287 ctx,
288 )
289 .await
290 .map(no_store)
291 }
292 pub(super) fn grant_openapi(
293 &self,
294 mut metadata: alibi_core::PluginOpenApiMetadata,
295 ) -> alibi_core::PluginOpenApiMetadata {
296 let Some(grant) = &self.config.grant else {
297 return metadata;
298 };
299 metadata.models.push(alibi_core::OpenApiModel::new(
300 "DeviceCode",
301 grant.device_code_schema_fields(),
302 ));
303 let mut properties = grant.request_schema_fields();
304 _ = properties.insert(
305 "scope".into(),
306 serde_json::json!({"type":"string","description":"Space-separated list of scopes"}),
307 );
308 _ = properties.insert("client_id".into(), serde_json::json!({"type":"string"}));
309 _ = properties.insert("user_id".into(), serde_json::json!({"type":"string","description":"The user ID to which the device code should be pre-bound."}));
310 let required = grant
311 .request_schema_fields()
312 .keys()
313 .cloned()
314 .collect::<Vec<_>>();
315 for (_, path, endpoint) in &mut metadata.endpoints {
316 if path == "/device/code" {
317 endpoint.request_body = Some(
318 serde_json::json!({"required":true,"content":{"application/json":{"schema":{"type":"object","properties":properties,"required":required}}}}),
319 );
320 if let Some(codes) = endpoint
321 .responses
322 .get_mut("400")
323 .and_then(|response| {
324 response
325 .pointer_mut("/content/application~1json/schema/properties/error/enum")
326 })
327 .and_then(Value::as_array_mut)
328 {
329 codes.extend(grant.request_error_codes().into_iter().map(Value::String));
330 }
331 for (status, response) in grant.request_openapi_responses() {
332 _ = endpoint.responses.insert(status, response);
333 }
334 }
335 if path == "/device" {
336 let response=endpoint.responses.entry("200".into()).or_insert_with(||serde_json::json!({"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}));
337 if let Some(properties) = response
338 .pointer_mut("/content/application~1json/schema/properties")
339 .and_then(Value::as_object_mut)
340 {
341 properties.extend(grant.verification_openapi_properties());
342 }
343 }
344 }
345 metadata
346 }
347}