Skip to main content

rustauth_plugins/device_authorization/routes/
decision.rs

1use 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}