headless_lms_models/library/oauth/
bearer.rs1use secrecy::SecretString;
4
5use crate::{
6 library::oauth::token_digest_sha256, oauth_access_token::TokenType, prelude::*, users::User,
7};
8
9#[derive(Debug, Clone)]
11pub struct BearerToken {
12 pub user: User,
13 pub scopes: Vec<String>,
16 pub audience: Option<Vec<String>>,
18 pub expires_at: DateTime<Utc>,
19 pub client_id: Uuid,
21 pub client_name: String,
22 pub is_metadata_document_client: bool,
23 pub grant: BearerGrant,
24}
25
26#[derive(Debug, Clone)]
28pub struct BearerGrant {
29 pub id: Uuid,
31 pub last_used_at: Option<DateTime<Utc>>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct TokenSubject {
39 pub user_id: Uuid,
40 pub grant_id: Uuid,
42}
43
44#[derive(Debug)]
46pub enum BearerTokenError {
47 Invalid(&'static str),
49 Database(ModelError),
51}
52
53pub 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
61pub 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
67pub 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}