openauth_plugins/device_authorization/routes/
decision.rs1use http::{header, Method, StatusCode};
2use openauth_core::api::{
3 create_auth_endpoint, parse_request_body, AuthEndpointOptions, BodyField, BodySchema,
4 JsonSchemaType,
5};
6use openauth_core::auth::session::{GetSessionInput, SessionAuth};
7use openauth_core::context::AuthContext;
8use openauth_core::db::{DbAdapter, User};
9use openauth_core::error::OpenAuthError;
10use openauth_core::plugin::PluginEndpoint;
11use serde::{Deserialize, Serialize};
12use time::OffsetDateTime;
13
14use crate::device_authorization::errors::{oauth_error_response, OAuthDeviceError};
15use crate::device_authorization::routes::token::required_adapter;
16use crate::device_authorization::store::{
17 DeviceAuthorizationStatus, DeviceCodeRecord, DeviceCodeStore,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
21pub struct DeviceApprovalRequest {
22 #[serde(rename = "userCode")]
23 pub user_code: String,
24}
25
26pub fn device_approve() -> PluginEndpoint {
27 decision_endpoint("/device/approve", "deviceApprove", Decision::Approve)
28}
29
30pub fn device_deny() -> PluginEndpoint {
31 decision_endpoint("/device/deny", "deviceDeny", Decision::Deny)
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum Decision {
36 Approve,
37 Deny,
38}
39
40fn decision_endpoint(
41 path: &'static str,
42 operation_id: &'static str,
43 decision: Decision,
44) -> PluginEndpoint {
45 create_auth_endpoint(
46 path,
47 Method::POST,
48 AuthEndpointOptions::new()
49 .operation_id(operation_id)
50 .allowed_media_types(["application/json", "application/x-www-form-urlencoded"])
51 .openapi(super::openapi::device_decision_operation(
52 operation_id,
53 match decision {
54 Decision::Approve => {
55 "Approve a pending OAuth 2.0 device authorization request."
56 }
57 Decision::Deny => "Deny a pending OAuth 2.0 device authorization request.",
58 },
59 ))
60 .body_schema(BodySchema::object([BodyField::new(
61 "userCode",
62 JsonSchemaType::String,
63 )])),
64 move |context, request| {
65 Box::pin(async move {
66 let adapter = required_adapter(context)?;
67 let Some(user) = authenticated_user(context, adapter.as_ref(), &request).await?
68 else {
69 return oauth_error_response(
70 StatusCode::UNAUTHORIZED,
71 OAuthDeviceError::Unauthorized,
72 "Authentication required",
73 );
74 };
75 let body = parse_request_body::<DeviceApprovalRequest>(&request)?;
76 let clean = super::clean_user_code(&body.user_code);
77 let store = DeviceCodeStore::new(adapter.as_ref());
78 let Some(record) = store.find_by_user_code(&clean).await? else {
79 return oauth_error_response(
80 StatusCode::BAD_REQUEST,
81 OAuthDeviceError::InvalidRequest,
82 "Invalid user code",
83 );
84 };
85 if record.expires_at < OffsetDateTime::now_utc() {
86 return oauth_error_response(
87 StatusCode::BAD_REQUEST,
88 OAuthDeviceError::ExpiredToken,
89 "User code has expired",
90 );
91 }
92 if record.status != DeviceAuthorizationStatus::Pending {
93 return oauth_error_response(
94 StatusCode::BAD_REQUEST,
95 OAuthDeviceError::InvalidRequest,
96 "Device code already processed",
97 );
98 }
99 if record
100 .user_id
101 .as_ref()
102 .is_some_and(|user_id| user_id != &user.id)
103 {
104 return oauth_error_response(
105 StatusCode::FORBIDDEN,
106 OAuthDeviceError::AccessDenied,
107 "You are not authorized to process this device authorization",
108 );
109 }
110 apply_decision(&store, &record, &user, decision).await?;
111 super::json_response(StatusCode::OK, &super::SuccessResponse { success: true })
112 })
113 },
114 )
115}
116
117async fn authenticated_user(
118 context: &AuthContext,
119 adapter: &dyn DbAdapter,
120 request: &openauth_core::api::ApiRequest,
121) -> Result<Option<User>, OpenAuthError> {
122 let cookie_header = request
123 .headers()
124 .get(header::COOKIE)
125 .and_then(|value| value.to_str().ok())
126 .unwrap_or_default();
127 let Some(result) = SessionAuth::new(adapter, context)
128 .get_session(GetSessionInput::new(cookie_header).disable_refresh())
129 .await?
130 else {
131 return Ok(None);
132 };
133 Ok(result.user)
134}
135
136async fn apply_decision(
137 store: &DeviceCodeStore<'_>,
138 record: &DeviceCodeRecord,
139 user: &User,
140 decision: Decision,
141) -> Result<(), OpenAuthError> {
142 match decision {
143 Decision::Approve => {
144 store.approve(&record.id, &user.id).await?;
145 }
146 Decision::Deny => {
147 store.deny(&record.id, &user.id).await?;
148 }
149 }
150 Ok(())
151}