Skip to main content

headless_lms_models/library/oauth/
bearer.rs

1//! `Authorization: Bearer` access tokens presented to this server's own resource APIs.
2
3use secrecy::SecretString;
4
5use crate::{
6    library::oauth::token_digest_sha256, oauth_access_token::TokenType, prelude::*, users::User,
7};
8
9/// A still-valid Bearer token of an active user, issued to a client that may use Bearer tokens.
10#[derive(Debug, Clone)]
11pub struct BearerToken {
12    pub user: User,
13    /// The token's scopes that its grant still allows: a grant narrowed by a later consent caps
14    /// the tokens issued before it.
15    pub scopes: Vec<String>,
16    /// The RFC 8707 resources the token is bound to; see [`super::is_bound_to`].
17    pub audience: Option<Vec<String>>,
18    pub expires_at: DateTime<Utc>,
19    /// `oauth_clients.id`.
20    pub client_id: Uuid,
21    pub client_name: String,
22    pub is_metadata_document_client: bool,
23    pub grant: BearerGrant,
24}
25
26/// The user's grant to the client a [`BearerToken`] was issued to.
27#[derive(Debug, Clone)]
28pub struct BearerGrant {
29    /// `oauth_user_client_scopes.id`.
30    pub id: Uuid,
31    /// `oauth_user_client_scopes.last_used_at`.
32    pub last_used_at: Option<DateTime<Utc>>,
33}
34
35/// Who a request authenticated with an access token acts for. Token-authenticating middlewares put
36/// it in the request extensions, where per-user and per-grant rate limits read it.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct TokenSubject {
39    pub user_id: Uuid,
40    /// `oauth_user_client_scopes.id`.
41    pub grant_id: Uuid,
42}
43
44/// Why a Bearer token wasn't accepted.
45#[derive(Debug)]
46pub enum BearerTokenError {
47    /// Answer 401. The message is safe to show to the client.
48    Invalid(&'static str),
49    /// Not the token's fault, so never a 401: clients drop their credentials on a 401.
50    Database(ModelError),
51}
52
53/// The scheme and credentials of an `Authorization` header value. Schemes are case-insensitive
54/// (RFC 9110 ยง11.1), so compare them with `eq_ignore_ascii_case`.
55pub fn split_authorization(header_value: &str) -> Option<(&str, &str)> {
56    let (scheme, credentials) = header_value.split_once(' ')?;
57    let credentials = credentials.trim_start();
58    (!credentials.is_empty()).then_some((scheme, credentials))
59}
60
61/// The token of an `Authorization: Bearer <token>` header value, for [`resolve_bearer_token`].
62pub fn bearer_token(header_value: &str) -> Option<&str> {
63    split_authorization(header_value)
64        .and_then(|(scheme, token)| scheme.eq_ignore_ascii_case("bearer").then_some(token))
65}
66
67/// Resolves a token presented as `Authorization: Bearer`. A sender-constrained (DPoP) token is
68/// rejected, since these APIs verify no proof of possession. Callers check scopes and audience.
69pub async fn resolve_bearer_token(
70    conn: &mut PgConnection,
71    token: &str,
72    token_hmac_key: &SecretString,
73) -> Result<BearerToken, BearerTokenError> {
74    let digest = token_digest_sha256(token, token_hmac_key);
75    let row = sqlx::query!(
76        r#"
77SELECT t.token_type,
78  t.scopes,
79  t.audience,
80  t.expires_at,
81  t.user_id,
82  c.id AS client_id,
83  c.client_name,
84  c.bearer_allowed,
85  c.metadata_document_fetched_at IS NOT NULL AS "is_metadata_document_client!",
86  u.id AS "active_user_id?",
87  u.created_at AS "user_created_at?",
88  u.updated_at AS "user_updated_at?",
89  u.upstream_id AS "user_upstream_id?",
90  u.email_domain AS "user_email_domain?",
91  g.id AS "grant_id?",
92  g.scopes AS "grant_scopes?",
93  g.last_used_at AS "grant_last_used_at?"
94FROM oauth_access_tokens t
95  JOIN oauth_clients c ON c.id = t.client_id
96  AND c.deleted_at IS NULL
97  LEFT JOIN users u ON u.id = t.user_id
98  AND u.deleted_at IS NULL
99  LEFT JOIN oauth_user_client_scopes g ON g.user_id = t.user_id
100  AND g.client_id = t.client_id
101WHERE t.digest = $1
102  AND t.expires_at > now()
103        "#,
104        digest.as_bytes()
105    )
106    .fetch_optional(conn)
107    .await
108    .map_err(|err| BearerTokenError::Database(err.into()))?;
109    let Some(row) = row else {
110        return Err(BearerTokenError::Invalid(
111            "The access token is missing, invalid, or expired.",
112        ));
113    };
114    if row.token_type != TokenType::Bearer {
115        return Err(BearerTokenError::Invalid(
116            "This API accepts only Bearer tokens; the presented token is sender-constrained.",
117        ));
118    }
119    if !row.bearer_allowed {
120        return Err(BearerTokenError::Invalid(
121            "The access token's client is not permitted to use Bearer tokens.",
122        ));
123    }
124    if row.user_id.is_none() {
125        return Err(BearerTokenError::Invalid(
126            "The access token is not associated with a user.",
127        ));
128    }
129    let (Some(id), Some(created_at), Some(updated_at)) =
130        (row.active_user_id, row.user_created_at, row.user_updated_at)
131    else {
132        return Err(BearerTokenError::Invalid(
133            "The access token's user could not be found.",
134        ));
135    };
136    let (Some(grant_id), Some(grant_scopes)) = (row.grant_id, row.grant_scopes) else {
137        return Err(BearerTokenError::Invalid(
138            "The access token's grant has been revoked.",
139        ));
140    };
141    let scopes = row
142        .scopes
143        .into_iter()
144        .filter(|scope| grant_scopes.contains(scope))
145        .collect();
146    Ok(BearerToken {
147        user: User {
148            id,
149            created_at,
150            updated_at,
151            deleted_at: None,
152            upstream_id: row.user_upstream_id,
153            email_domain: row.user_email_domain,
154        },
155        scopes,
156        audience: row.audience,
157        expires_at: row.expires_at,
158        client_id: row.client_id,
159        client_name: row.client_name,
160        is_metadata_document_client: row.is_metadata_document_client,
161        grant: BearerGrant {
162            id: grant_id,
163            last_used_at: row.grant_last_used_at,
164        },
165    })
166}
167
168#[cfg(test)]
169mod tests {
170    use super::bearer_token;
171
172    #[test]
173    fn the_bearer_scheme_is_case_insensitive() {
174        assert_eq!(bearer_token("Bearer abc"), Some("abc"));
175        assert_eq!(bearer_token("bearer abc"), Some("abc"));
176        assert_eq!(bearer_token("BEARER  abc"), Some("abc"));
177        assert_eq!(bearer_token("Basic abc"), None);
178        assert_eq!(bearer_token("Bearer"), None);
179        assert_eq!(bearer_token("Bearer "), None);
180    }
181}