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