use super::types::{
DeviceActionRequest, DeviceActionResponse, DeviceCodeRequest, DeviceTokenRequest,
DeviceVerifyResponse,
};
use super::{
AUTHENTICATION_REQUIRED, DEVICE_CODE_ALREADY_PROCESSED, DEVICE_CODE_NOT_CLAIMED,
DEVICE_STATUS_PENDING, DeviceAuthorizationPlugin, DeviceDecision, DeviceGrantRecord,
DeviceRequestKind, EXPIRED_USER_CODE, INVALID_REQUEST, INVALID_USER_CODE,
deserialize_device_body, device_error_response, find_device_code_by_user_code, no_store,
parse_device_body, set_device_no_store_headers, validate_device_media,
};
use alibi_core::entity::AuthUser;
use alibi_core::{AuthContext, AuthError, AuthRequest, AuthResponse, AuthResult, UpdateDeviceCode};
use chrono::Utc;
impl DeviceAuthorizationPlugin {
pub(in crate::device_authorization) async fn handle_device_code(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
if let Some(grant) = &self.config.grant {
return self
.handle_application_issuance(req, ctx, grant.as_ref())
.await;
}
let body: DeviceCodeRequest = match parse_device_body(req, DeviceRequestKind::Issuance)
.and_then(deserialize_device_body)
{
Ok(value) => value,
Err(response) => return Ok(response),
};
self.issue_device_code(body, ctx)
.await
.inspect_err(|error| {
if alibi_core::endpoint::is_endpoint_api_error(error) {
set_device_no_store_headers(req);
}
})
.map(no_store)
}
pub(in crate::device_authorization) async fn handle_device_token(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let body: DeviceTokenRequest = match parse_device_body(req, DeviceRequestKind::Token)
.and_then(deserialize_device_body)
{
Ok(value) => value,
Err(response) => return Ok(response),
};
self.redeem_device_token(body, req, ctx)
.await
.inspect_err(|error| {
if alibi_core::endpoint::is_endpoint_api_error(error) {
set_device_no_store_headers(req);
}
})
.map(no_store)
}
pub(in crate::device_authorization) async fn handle_device_verify(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
let Some(user_code) = req.query.get("user_code").cloned() else {
return device_error_response(400, "invalid_request", INVALID_REQUEST);
};
self.verify_core(req, ctx, user_code).await
}
pub(super) async fn verify_core(
&self,
req: &impl alibi_core::session::SessionRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
user_code: String,
) -> AuthResult<AuthResponse> {
let Some(mut device_code) = find_device_code_by_user_code(ctx, &user_code).await? else {
return device_error_response(400, "invalid_request", INVALID_USER_CODE);
};
if device_code.expires_at < Utc::now() {
return device_error_response(400, "expired_token", EXPIRED_USER_CODE);
}
let user_id = match ctx.require_cached_session(req).await {
Ok((user, _)) => Some(user.id().into_owned()),
Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => None,
Err(err) => return Err(err),
};
if device_code.user_id.is_none()
&& device_code.status == DEVICE_STATUS_PENDING
&& let Some(user_id) = user_id.as_deref()
&& ctx
.database
.claim_device_code(&device_code.id, user_id)
.await?
{
device_code.user_id = Some(user_id.to_owned());
}
let can_review_request = user_id.is_some() && device_code.user_id == user_id;
let mut response = serde_json::to_value(DeviceVerifyResponse {
user_code,
status: device_code.status.clone(),
client_id: can_review_request.then_some(device_code.client_id.clone()),
scope: can_review_request.then_some(device_code.scope.clone()),
})?;
if can_review_request && let Some(grant) = &self.config.grant {
let fields = ctx.database.device_code_fields(&device_code.id).await?;
let record = DeviceGrantRecord {
device_code,
fields,
};
if let Some(object) = response.as_object_mut() {
object.extend(grant.verification_context(&record).await?);
}
}
AuthResponse::json(200, &response).map_err(AuthError::from)
}
pub(in crate::device_authorization) async fn handle_device_approve(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
self.handle_device_decision(req, ctx, DeviceDecision::Approve)
.await
}
pub(in crate::device_authorization) async fn handle_device_deny(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
) -> AuthResult<AuthResponse> {
self.handle_device_decision(req, ctx, DeviceDecision::Deny)
.await
}
pub(in crate::device_authorization) async fn handle_device_decision(
&self,
req: &AuthRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
decision: DeviceDecision,
) -> AuthResult<AuthResponse> {
if let Err(response) = validate_device_media(req, false) {
return Ok(response);
}
let body: DeviceActionRequest = match parse_device_body(req, DeviceRequestKind::Decision)
.and_then(deserialize_device_body)
{
Ok(value) => value,
Err(response) => return Ok(response),
};
self.decision_core(req, ctx, decision, body).await
}
pub(super) async fn decision_core(
&self,
req: &impl alibi_core::session::SessionRequest,
ctx: &AuthContext<impl alibi_core::AuthSchema>,
decision: DeviceDecision,
body: DeviceActionRequest,
) -> AuthResult<AuthResponse> {
let user = match ctx.require_cached_session(req).await {
Ok((user, _session)) => user,
Err(AuthError::Unauthenticated | AuthError::SessionNotFound) => {
return device_error_response(401, "unauthorized", AUTHENTICATION_REQUIRED);
}
Err(error) => return Err(error),
};
let current_user_id = user.id().into_owned();
let Some(device_code) = find_device_code_by_user_code(ctx, &body.user_code).await? else {
return device_error_response(400, "invalid_request", INVALID_USER_CODE);
};
if device_code.expires_at < Utc::now() {
return device_error_response(400, "expired_token", EXPIRED_USER_CODE);
}
if device_code.status != DEVICE_STATUS_PENDING {
return device_error_response(400, "invalid_request", DEVICE_CODE_ALREADY_PROCESSED);
}
let Some(claimed_user_id) = device_code.user_id.as_deref() else {
return device_error_response(400, "invalid_request", DEVICE_CODE_NOT_CLAIMED);
};
if claimed_user_id != current_user_id {
return device_error_response(403, "access_denied", decision.forbidden_message());
}
let updated_user_id = claimed_user_id.to_owned();
_ = ctx
.database
.update_device_code(
&device_code.id,
UpdateDeviceCode {
status: Some(decision.status().to_owned()),
user_id: Some(Some(updated_user_id)),
..Default::default()
},
)
.await?;
AuthResponse::json(200, &DeviceActionResponse { success: true }).map_err(AuthError::from)
}
}