headless_lms_server/domain/oauth/
helpers.rs1use crate::domain::error::{OAuthErrorCode, OAuthErrorData};
2use crate::prelude::*;
3use models::{library::oauth::token_digest_sha256, oauth_client::OAuthClient};
4use secrecy::{ExposeSecret, SecretString};
5
6#[track_caller]
10pub fn oauth_error(
11 code: OAuthErrorCode,
12 description: impl Into<String>,
13 redirect: Option<&str>,
14 state: Option<&str>,
15) -> ControllerError {
16 let description = description.into();
17 controller_err!(
18 OAuthError(Box::new(OAuthErrorData {
19 redirect_uri: redirect.map(str::to_string),
20 state: state.map(str::to_string),
21 ..OAuthErrorData::new(code, description.clone())
22 })),
23 description
24 )
25}
26
27#[track_caller]
29pub fn oauth_server_error(description: &'static str, source: anyhow::Error) -> ControllerError {
30 controller_err!(
31 OAuthError(Box::new(OAuthErrorData::new(
32 OAuthErrorCode::ServerError,
33 description,
34 ))),
35 description,
36 source
37 )
38}
39
40#[track_caller]
41pub fn oauth_invalid_request(
42 description: impl Into<String>,
43 redirect: Option<&str>,
44 state: Option<&str>,
45) -> ControllerError {
46 oauth_error(OAuthErrorCode::InvalidRequest, description, redirect, state)
47}
48
49#[track_caller]
51pub fn oauth_invalid_client(description: impl Into<String>) -> ControllerError {
52 oauth_error(OAuthErrorCode::InvalidClient, description, None, None)
53}
54
55#[track_caller]
57pub fn oauth_invalid_token(description: impl Into<String>) -> ControllerError {
58 oauth_error(OAuthErrorCode::InvalidToken, description, None, None)
59}
60
61#[track_caller]
63pub fn oauth_invalid_scope(description: impl Into<String>) -> ControllerError {
64 oauth_error(OAuthErrorCode::InvalidScope, description, None, None)
65}
66
67#[track_caller]
69pub fn oauth_unauthorized_client(description: impl Into<String>) -> ControllerError {
70 oauth_error(OAuthErrorCode::UnauthorizedClient, description, None, None)
71}
72
73#[track_caller]
75pub fn oauth_invalid_grant(description: impl Into<String>) -> ControllerError {
76 oauth_error(OAuthErrorCode::InvalidGrant, description, None, None)
77}
78
79pub fn scope_has_openid(scope: &[String]) -> bool {
80 scope.iter().any(|s| s == "openid")
81}
82
83pub fn split_and_validate_scopes(
88 requested: &str,
89 allowed: &[String],
90) -> Result<Vec<String>, String> {
91 let scopes: Vec<String> = requested
92 .split_whitespace()
93 .map(|s| s.to_string())
94 .collect();
95 for scope in &scopes {
96 if !allowed.contains(scope) {
97 return Err(scope.clone());
98 }
99 }
100 Ok(scopes)
101}
102
103pub fn ok_json_no_cache<T: Serialize>(value: T) -> HttpResponse {
104 let mut resp = HttpResponse::Ok();
105 resp.insert_header(("Cache-Control", "no-store"));
106 resp.insert_header(("Pragma", "no-cache"));
107 resp.json(value)
108}
109
110pub enum ClientAuthError {
112 UnknownClient,
113 ClientSecretMissing,
114 ClientSecretMismatch,
115}
116
117pub async fn authenticate_oauth_client(
123 conn: &mut PgConnection,
124 client_id: &str,
125 provided_secret: Option<&SecretString>,
126 token_hmac_key: &SecretString,
127) -> Result<OAuthClient, ClientAuthError> {
128 let client = OAuthClient::find_by_client_id(conn, client_id)
129 .await
130 .map_err(|e| {
131 tracing::error!(err = %e, "OAuth: unknown client_id");
132 ClientAuthError::UnknownClient
133 })?;
134
135 if !client.is_confidential() {
136 return Ok(client);
137 }
138
139 let Some(secret) = &client.client_secret else {
140 return Err(ClientAuthError::ClientSecretMissing);
141 };
142 let provided = token_digest_sha256(
143 provided_secret
144 .map(|s| s.expose_secret())
145 .unwrap_or_default(),
146 token_hmac_key,
147 );
148 if !secret.constant_eq(&provided) {
149 return Err(ClientAuthError::ClientSecretMismatch);
150 }
151
152 Ok(client)
153}