Skip to main content

lenso_http_auth/
lib.rs

1//! Auth-aware actor extraction for authored HTTP Endpoint providers.
2//!
3//! This crate owns ingress authentication glue only. Extracted actors identify
4//! evidence accepted by the bound Auth Plugin; target Plugins must still verify
5//! the attached assertion and make the final business authorization decision.
6
7use std::rc::Rc;
8
9use lenso_auth_sdk::{
10    ActorAssertion, AuthOutcome, CredentialEvidence, authenticate_request, decode_auth_response,
11};
12use lenso_capability_auth::{AuthClient, AuthInvocationError, AuthenticateError};
13use lenso_capability_http_endpoint::{
14    EndpointHandleInvocationError, ExtractorFuture, HandleRequest, HandleResponse,
15    response::{self, HeaderValue, StatusCode, header},
16};
17use lenso_kernel::{InvocationContext, RuntimeFailure};
18
19/// Supplies the activation-time Auth client used during request extraction.
20pub trait AuthClientSource {
21    /// Returns the client bound to this HTTP Endpoint provider.
22    fn auth_client(&self) -> Result<Rc<AuthClient>, EndpointHandleInvocationError>;
23}
24
25/// An application-owned actor projection at the HTTP authentication boundary.
26///
27/// `KIND` distinguishes credential actor kinds such as `user` or `admin`.
28/// Roles, permissions, tenant access, and resource ownership remain target-owned
29/// authorization concerns and must not be decided by this projection.
30/// This is deliberately separate from `lenso_auth_sdk::TypedActor`, which is
31/// projected only after the target verifies assertion provenance, audience,
32/// validity, and proof.
33pub trait AuthenticatedHttpActor: Sized {
34    /// Auth assertion actor kind accepted by this extractor.
35    const KIND: &'static str;
36
37    /// Builds the application's ingress actor after the kind has matched.
38    fn from_assertion(assertion: &ActorAssertion) -> Self;
39}
40
41/// Authenticates request evidence, attaches the assertion, and projects the
42/// HTTP-edge identity `A`.
43///
44/// An application implements [`AuthClientSource`] for its Endpoint provider and
45/// delegates its `FromRequest` implementation to this function. Authentication
46/// failures become intentional HTTP responses; runtime failures keep their
47/// generated Endpoint invocation semantics.
48pub fn extract_authenticated_actor<'a, P, A>(
49    provider: &'a P,
50    context: &'a mut InvocationContext,
51    request: &'a HandleRequest,
52) -> ExtractorFuture<'a, A>
53where
54    P: AuthClientSource + ?Sized,
55    A: AuthenticatedHttpActor + 'a,
56{
57    Box::pin(async move {
58        let auth = provider.auth_client()?;
59        let evidence = request.credential.as_ref().map(|credential| {
60            CredentialEvidence::new(credential.scheme.clone(), credential.value.clone())
61        });
62        let response = match auth
63            .authenticate_with_context(context.clone(), authenticate_request(evidence))
64            .await
65        {
66            Ok(response) => response,
67            Err(AuthInvocationError::Domain(error)) => {
68                return Err(authentication_rejection(&error)?.into());
69            }
70            Err(AuthInvocationError::Runtime(error)) => {
71                return Err(EndpointHandleInvocationError::Runtime(error).into());
72            }
73        };
74        let assertion = match decode_auth_response(response).map_err(internal)? {
75            AuthOutcome::Absent => {
76                return Err(unauthorized(
77                    "authentication_required",
78                    "Authentication credentials are required.",
79                )?
80                .into());
81            }
82            AuthOutcome::Authenticated(assertion) => assertion,
83        };
84        if assertion.actor_kind() != A::KIND {
85            return Err(response::problem(
86                StatusCode::FORBIDDEN,
87                "unexpected_actor_kind",
88                "The authenticated actor cannot access this endpoint.",
89            )
90            .into());
91        }
92        *context = assertion.attach(context.clone()).map_err(internal)?;
93        Ok(A::from_assertion(&assertion))
94    })
95}
96
97fn authentication_rejection(
98    error: &AuthenticateError,
99) -> Result<HandleResponse, EndpointHandleInvocationError> {
100    let (code, detail) = match error {
101        AuthenticateError::Expired => (
102            "expired_credential",
103            "The supplied authentication credential has expired.",
104        ),
105        AuthenticateError::Invalid => (
106            "invalid_credential",
107            "The supplied authentication credential is invalid.",
108        ),
109        AuthenticateError::Revoked => (
110            "revoked_credential",
111            "The supplied authentication credential has been revoked.",
112        ),
113        AuthenticateError::Unsupported => (
114            "unsupported_credential",
115            "The supplied authentication credential is not supported.",
116        ),
117        AuthenticateError::Unknown(_) => (
118            "authentication_failed",
119            "The supplied authentication credential was not accepted.",
120        ),
121    };
122    unauthorized(code, detail)
123}
124
125fn unauthorized(
126    code: &'static str,
127    detail: &'static str,
128) -> Result<HandleResponse, EndpointHandleInvocationError> {
129    Ok(
130        response::problem(StatusCode::UNAUTHORIZED, code, detail).with_header(
131            &header::WWW_AUTHENTICATE,
132            &HeaderValue::from_static("Bearer"),
133        )?,
134    )
135}
136
137fn internal(error: impl std::fmt::Debug) -> EndpointHandleInvocationError {
138    EndpointHandleInvocationError::Runtime(RuntimeFailure::Internal {
139        detail: format!("{error:?}"),
140    })
141}