pub use super::types::{
DeviceActionResponse, DeviceCodeRequest, DeviceCodeResponse, DeviceTokenResponse,
};
use super::{DeviceAuthorizationPlugin, DeviceRequestKind, no_store, validate_device_json_body};
use alibi_core::endpoint::{
EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
};
use alibi_core::utils::json::JsValue;
use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
pub(super) fn definitions() -> Vec<EndpointDefinition> {
vec![
crate::endpoint::definition(
"deviceCode",
"deviceCode",
Some("/device/code"),
HttpMethod::Post,
),
crate::endpoint::definition(
"deviceVerify",
"deviceVerify",
Some("/device"),
HttpMethod::Get,
),
crate::endpoint::definition(
"deviceApprove",
"deviceApprove",
Some("/device/approve"),
HttpMethod::Post,
),
crate::endpoint::definition(
"deviceDeny",
"deviceDeny",
Some("/device/deny"),
HttpMethod::Post,
),
crate::endpoint::definition(
"deviceToken",
"deviceToken",
Some("/device/token"),
HttpMethod::Post,
),
]
}
pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
let body = if matches!(call.operation_id(), "deviceApprove" | "deviceDeny") {
let body = crate::endpoint::validate_fields(
call.body(),
"body",
&[crate::authentication_helpers::JsonField::string(
"userCode", true,
)],
)?;
if call.headers().is_none() {
return Err(crate::endpoint::validation("Headers is required"));
}
Some(body)
} else if call.operation_id() == "deviceToken" {
Some(validate_token_body(call.body())?)
} else {
call.body().cloned()
};
Ok(EndpointInput {
body,
query: if call.operation_id() == "deviceVerify" {
Some(crate::endpoint::validate_fields(
call.query(),
"query",
&[crate::authentication_helpers::JsonField::string(
"user_code",
true,
)],
)?)
} else {
call.query().cloned()
},
})
}
fn validate_token_body(body: Option<&JsValue>) -> AuthResult<JsValue> {
use crate::authentication_helpers::{JsonFieldKind, field_issue, json_type};
let Some(object) = body.and_then(JsValue::as_object) else {
return Err(crate::endpoint::validation(format!(
"[body] Invalid input: expected object, received {}",
json_type(body)
)));
};
let mut issues = Vec::new();
if object.get("grant_type").and_then(JsValue::as_str) != Some(super::DEVICE_GRANT_TYPE) {
issues.push(format!(
"[body.grant_type] Invalid input: expected \"{}\"",
super::DEVICE_GRANT_TYPE
));
}
for field in ["device_code", "client_id"] {
let value = object.get(field);
if let Some(issue) = field_issue(JsonFieldKind::String, value, json_type(value)) {
issues.push(format!("[body.{field}] {issue}"));
}
}
if !issues.is_empty() {
return Err(crate::endpoint::validation(issues.join("; ")));
}
Ok(JsValue::Object(
object
.iter()
.filter(|(name, _)| ["grant_type", "device_code", "client_id"].contains(&name.as_str()))
.map(|(name, value)| (name.clone(), value.clone()))
.collect(),
))
}
impl DeviceAuthorizationPlugin {
#[must_use]
pub fn device_token_endpoint(
device_code: impl Into<String>,
client_id: impl Into<String>,
) -> ServerEndpoint<DeviceTokenResponse> {
ServerEndpoint::new("device-authorization", "deviceToken").with_body_value(JsValue::Object(
[
(
"grant_type".into(),
JsValue::String(super::DEVICE_GRANT_TYPE.into()),
),
("device_code".into(), JsValue::String(device_code.into())),
("client_id".into(), JsValue::String(client_id.into())),
]
.into_iter()
.collect(),
))
}
pub(super) async fn token_endpoint<S: AuthSchema>(
&self,
call: &EndpointCall,
ctx: &AuthContext<S>,
) -> AuthResult<EndpointResponse> {
call.set_response_header("Cache-Control", "no-store");
call.set_response_header("Pragma", "no-cache");
endpoint_response(no_store(
self.redeem_device_token(call.body_as()?, call, ctx).await?,
))
}
#[must_use]
pub fn device_approve_endpoint(
user_code: impl Into<String>,
) -> ServerEndpoint<DeviceActionResponse> {
ServerEndpoint::new("device-authorization", "deviceApprove").with_body_value(
JsValue::Object(
[("userCode".into(), JsValue::String(user_code.into()))]
.into_iter()
.collect(),
),
)
}
#[must_use]
pub fn device_deny_endpoint(
user_code: impl Into<String>,
) -> ServerEndpoint<DeviceActionResponse> {
ServerEndpoint::new("device-authorization", "deviceDeny").with_body_value(JsValue::Object(
[("userCode".into(), JsValue::String(user_code.into()))]
.into_iter()
.collect(),
))
}
pub(super) async fn decision_endpoint<S: AuthSchema>(
&self,
call: &EndpointCall,
ctx: &AuthContext<S>,
) -> AuthResult<EndpointResponse> {
let decision = if call.operation_id() == "deviceDeny" {
super::DeviceDecision::Deny
} else {
super::DeviceDecision::Approve
};
endpoint_response(
self.decision_core(call, ctx, decision, call.body_as()?)
.await?,
)
}
#[must_use]
pub fn device_verify_endpoint(
user_code: impl Into<String>,
) -> ServerEndpoint<serde_json::Value> {
ServerEndpoint::new("device-authorization", "deviceVerify").with_query_value(
JsValue::Object(
[("user_code".into(), JsValue::String(user_code.into()))]
.into_iter()
.collect(),
),
)
}
pub(super) async fn verify_endpoint<S: AuthSchema>(
&self,
call: &EndpointCall,
ctx: &AuthContext<S>,
) -> AuthResult<EndpointResponse> {
#[derive(serde::Deserialize)]
struct Query {
user_code: String,
}
let query: Query = call.query_as()?;
let output = self.verify_core(call, ctx, query.user_code).await?;
endpoint_response(output)
}
pub fn device_code_endpoint(
request: &DeviceCodeRequest,
) -> AuthResult<ServerEndpoint<DeviceCodeResponse>> {
ServerEndpoint::new("device-authorization", "deviceCode").with_body(request)
}
pub(super) async fn issue_endpoint<S: AuthSchema>(
&self,
call: &EndpointCall,
ctx: &AuthContext<S>,
) -> AuthResult<EndpointResponse> {
let body: serde_json::Value = call
.body()
.map_or_else(|| Ok(serde_json::Value::Null), serde_json::to_value)?;
let output = if let Some(grant) = &self.config.grant {
self.application_issuance_body(body, call.request(), ctx, grant.as_ref())
.await?
} else {
match validate_device_json_body(&body, DeviceRequestKind::Issuance) {
Ok(()) => {
call.set_response_header("Cache-Control", "no-store");
call.set_response_header("Pragma", "no-cache");
no_store(
self.issue_device_code(serde_json::from_value(body)?, ctx)
.await?,
)
}
Err(output) => output,
}
};
endpoint_response(output)
}
}
fn endpoint_response(output: alibi_core::AuthResponse) -> AuthResult<EndpointResponse> {
let value = alibi_core::utils::json::from_slice::<JsValue>(&output.body)?;
let mut response = if output.status >= 400 {
EndpointResponse::error(crate::endpoint::error_response(output.clone()))
.with_error_body(value)
} else {
EndpointResponse::value(value)
};
response.set_status(Some(output.status));
response.merge_headers(output.headers);
Ok(response)
}