Skip to main content

openauth_plugins/device_authorization/routes/
decision.rs

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