alibi_plugins/device_authorization/
endpoint.rs1pub use super::types::{
3 DeviceActionResponse, DeviceCodeRequest, DeviceCodeResponse, DeviceTokenResponse,
4};
5use super::{DeviceAuthorizationPlugin, DeviceRequestKind, no_store, validate_device_json_body};
6use alibi_core::endpoint::{
7 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
8};
9use alibi_core::utils::json::JsValue;
10use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
11
12pub(super) fn definitions() -> Vec<EndpointDefinition> {
13 vec![
14 crate::endpoint::definition(
15 "deviceCode",
16 "deviceCode",
17 Some("/device/code"),
18 HttpMethod::Post,
19 ),
20 crate::endpoint::definition(
21 "deviceVerify",
22 "deviceVerify",
23 Some("/device"),
24 HttpMethod::Get,
25 ),
26 crate::endpoint::definition(
27 "deviceApprove",
28 "deviceApprove",
29 Some("/device/approve"),
30 HttpMethod::Post,
31 ),
32 crate::endpoint::definition(
33 "deviceDeny",
34 "deviceDeny",
35 Some("/device/deny"),
36 HttpMethod::Post,
37 ),
38 crate::endpoint::definition(
39 "deviceToken",
40 "deviceToken",
41 Some("/device/token"),
42 HttpMethod::Post,
43 ),
44 ]
45}
46
47pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
48 let body = if matches!(call.operation_id(), "deviceApprove" | "deviceDeny") {
49 let body = crate::endpoint::validate_fields(
50 call.body(),
51 "body",
52 &[crate::authentication_helpers::JsonField::string(
53 "userCode", true,
54 )],
55 )?;
56 if call.headers().is_none() {
57 return Err(crate::endpoint::validation("Headers is required"));
58 }
59 Some(body)
60 } else if call.operation_id() == "deviceToken" {
61 Some(validate_token_body(call.body())?)
62 } else {
63 call.body().cloned()
64 };
65 Ok(EndpointInput {
66 body,
67 query: if call.operation_id() == "deviceVerify" {
68 Some(crate::endpoint::validate_fields(
69 call.query(),
70 "query",
71 &[crate::authentication_helpers::JsonField::string(
72 "user_code",
73 true,
74 )],
75 )?)
76 } else {
77 call.query().cloned()
78 },
79 })
80}
81
82fn validate_token_body(body: Option<&JsValue>) -> AuthResult<JsValue> {
83 use crate::authentication_helpers::{JsonFieldKind, field_issue, json_type};
84 let Some(object) = body.and_then(JsValue::as_object) else {
85 return Err(crate::endpoint::validation(format!(
86 "[body] Invalid input: expected object, received {}",
87 json_type(body)
88 )));
89 };
90 let mut issues = Vec::new();
91 if object.get("grant_type").and_then(JsValue::as_str) != Some(super::DEVICE_GRANT_TYPE) {
92 issues.push(format!(
93 "[body.grant_type] Invalid input: expected \"{}\"",
94 super::DEVICE_GRANT_TYPE
95 ));
96 }
97 for field in ["device_code", "client_id"] {
98 let value = object.get(field);
99 if let Some(issue) = field_issue(JsonFieldKind::String, value, json_type(value)) {
100 issues.push(format!("[body.{field}] {issue}"));
101 }
102 }
103 if !issues.is_empty() {
104 return Err(crate::endpoint::validation(issues.join("; ")));
105 }
106 Ok(JsValue::Object(
107 object
108 .iter()
109 .filter(|(name, _)| ["grant_type", "device_code", "client_id"].contains(&name.as_str()))
110 .map(|(name, value)| (name.clone(), value.clone()))
111 .collect(),
112 ))
113}
114
115impl DeviceAuthorizationPlugin {
116 #[must_use]
118 pub fn device_token_endpoint(
119 device_code: impl Into<String>,
120 client_id: impl Into<String>,
121 ) -> ServerEndpoint<DeviceTokenResponse> {
122 ServerEndpoint::new("device-authorization", "deviceToken").with_body_value(JsValue::Object(
123 [
124 (
125 "grant_type".into(),
126 JsValue::String(super::DEVICE_GRANT_TYPE.into()),
127 ),
128 ("device_code".into(), JsValue::String(device_code.into())),
129 ("client_id".into(), JsValue::String(client_id.into())),
130 ]
131 .into_iter()
132 .collect(),
133 ))
134 }
135
136 pub(super) async fn token_endpoint<S: AuthSchema>(
137 &self,
138 call: &EndpointCall,
139 ctx: &AuthContext<S>,
140 ) -> AuthResult<EndpointResponse> {
141 call.set_response_header("Cache-Control", "no-store");
142 call.set_response_header("Pragma", "no-cache");
143 endpoint_response(no_store(
144 self.redeem_device_token(call.body_as()?, call, ctx).await?,
145 ))
146 }
147
148 #[must_use]
150 pub fn device_approve_endpoint(
151 user_code: impl Into<String>,
152 ) -> ServerEndpoint<DeviceActionResponse> {
153 ServerEndpoint::new("device-authorization", "deviceApprove").with_body_value(
154 JsValue::Object(
155 [("userCode".into(), JsValue::String(user_code.into()))]
156 .into_iter()
157 .collect(),
158 ),
159 )
160 }
161
162 #[must_use]
164 pub fn device_deny_endpoint(
165 user_code: impl Into<String>,
166 ) -> ServerEndpoint<DeviceActionResponse> {
167 ServerEndpoint::new("device-authorization", "deviceDeny").with_body_value(JsValue::Object(
168 [("userCode".into(), JsValue::String(user_code.into()))]
169 .into_iter()
170 .collect(),
171 ))
172 }
173
174 pub(super) async fn decision_endpoint<S: AuthSchema>(
175 &self,
176 call: &EndpointCall,
177 ctx: &AuthContext<S>,
178 ) -> AuthResult<EndpointResponse> {
179 let decision = if call.operation_id() == "deviceDeny" {
180 super::DeviceDecision::Deny
181 } else {
182 super::DeviceDecision::Approve
183 };
184 endpoint_response(
185 self.decision_core(call, ctx, decision, call.body_as()?)
186 .await?,
187 )
188 }
189
190 #[must_use]
192 pub fn device_verify_endpoint(
193 user_code: impl Into<String>,
194 ) -> ServerEndpoint<serde_json::Value> {
195 ServerEndpoint::new("device-authorization", "deviceVerify").with_query_value(
196 JsValue::Object(
197 [("user_code".into(), JsValue::String(user_code.into()))]
198 .into_iter()
199 .collect(),
200 ),
201 )
202 }
203
204 pub(super) async fn verify_endpoint<S: AuthSchema>(
205 &self,
206 call: &EndpointCall,
207 ctx: &AuthContext<S>,
208 ) -> AuthResult<EndpointResponse> {
209 #[derive(serde::Deserialize)]
210 struct Query {
211 user_code: String,
212 }
213 let query: Query = call.query_as()?;
214 let output = self.verify_core(call, ctx, query.user_code).await?;
215 endpoint_response(output)
216 }
217
218 pub fn device_code_endpoint(
222 request: &DeviceCodeRequest,
223 ) -> AuthResult<ServerEndpoint<DeviceCodeResponse>> {
224 ServerEndpoint::new("device-authorization", "deviceCode").with_body(request)
225 }
226
227 pub(super) async fn issue_endpoint<S: AuthSchema>(
228 &self,
229 call: &EndpointCall,
230 ctx: &AuthContext<S>,
231 ) -> AuthResult<EndpointResponse> {
232 let body: serde_json::Value = call
233 .body()
234 .map_or_else(|| Ok(serde_json::Value::Null), serde_json::to_value)?;
235 let output = if let Some(grant) = &self.config.grant {
236 self.application_issuance_body(body, call.request(), ctx, grant.as_ref())
237 .await?
238 } else {
239 match validate_device_json_body(&body, DeviceRequestKind::Issuance) {
240 Ok(()) => {
241 call.set_response_header("Cache-Control", "no-store");
242 call.set_response_header("Pragma", "no-cache");
243 no_store(
244 self.issue_device_code(serde_json::from_value(body)?, ctx)
245 .await?,
246 )
247 }
248 Err(output) => output,
249 }
250 };
251 endpoint_response(output)
252 }
253}
254
255fn endpoint_response(output: alibi_core::AuthResponse) -> AuthResult<EndpointResponse> {
256 let value = alibi_core::utils::json::from_slice::<JsValue>(&output.body)?;
257 let mut response = if output.status >= 400 {
258 EndpointResponse::error(crate::endpoint::error_response(output.clone()))
259 .with_error_body(value)
260 } else {
261 EndpointResponse::value(value)
262 };
263 response.set_status(Some(output.status));
264 response.merge_headers(output.headers);
265 Ok(response)
266}