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
6pub fn oauth_error(
7 error: &'static str,
8 desc: &'static str,
9 redirect: Option<&str>,
10 state: Option<&str>,
11) -> ControllerError {
12 ControllerError::new(
13 ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
14 error: error.into(),
15 error_description: desc.into(),
16 redirect_uri: redirect.map(str::to_string),
17 state: state.map(str::to_string),
18 nonce: None,
19 })),
20 desc,
21 None::<anyhow::Error>,
22 )
23}
24
25pub fn oauth_invalid_request(
26 desc: &'static str,
27 redirect: Option<&str>,
28 state: Option<&str>,
29) -> ControllerError {
30 oauth_error(
31 OAuthErrorCode::InvalidRequest.as_str(),
32 desc,
33 redirect,
34 state,
35 )
36}
37
38pub fn oauth_invalid_client(desc: &'static str) -> ControllerError {
39 oauth_error(OAuthErrorCode::InvalidClient.as_str(), desc, None, None)
40}
41
42pub fn oauth_invalid_scope(desc: &'static str) -> ControllerError {
43 oauth_error(OAuthErrorCode::InvalidScope.as_str(), desc, None, None)
44}
45
46pub fn oauth_unauthorized_client(desc: &'static str) -> ControllerError {
47 oauth_error(
48 OAuthErrorCode::UnauthorizedClient.as_str(),
49 desc,
50 None,
51 None,
52 )
53}
54
55pub fn oauth_invalid_grant(desc: &'static str) -> ControllerError {
56 oauth_error(OAuthErrorCode::InvalidGrant.as_str(), desc, None, None)
57}
58
59pub fn scope_has_openid(scope: &[String]) -> bool {
60 scope.iter().any(|s| s == "openid")
61}
62
63pub fn split_and_validate_scopes(
68 requested: &str,
69 allowed: &[String],
70) -> Result<Vec<String>, String> {
71 let scopes: Vec<String> = requested
72 .split_whitespace()
73 .map(|s| s.to_string())
74 .collect();
75 for scope in &scopes {
76 if !allowed.contains(scope) {
77 return Err(scope.clone());
78 }
79 }
80 Ok(scopes)
81}
82
83pub fn ok_json_no_cache<T: Serialize>(value: T) -> HttpResponse {
84 let mut resp = HttpResponse::Ok();
85 resp.insert_header(("Cache-Control", "no-store"));
86 resp.insert_header(("Pragma", "no-cache"));
87 resp.json(value)
88}
89
90pub enum ClientAuthError {
92 UnknownClient,
93 ClientSecretMissing,
94 ClientSecretMismatch,
95}
96
97pub async fn authenticate_oauth_client(
103 conn: &mut PgConnection,
104 client_id: &str,
105 provided_secret: Option<&SecretString>,
106 token_hmac_key: &SecretString,
107) -> Result<OAuthClient, ClientAuthError> {
108 let client = OAuthClient::find_by_client_id(conn, client_id)
109 .await
110 .map_err(|e| {
111 tracing::error!(err = %e, "OAuth: unknown client_id");
112 ClientAuthError::UnknownClient
113 })?;
114
115 if !client.is_confidential() {
116 return Ok(client);
117 }
118
119 let Some(secret) = &client.client_secret else {
120 return Err(ClientAuthError::ClientSecretMissing);
121 };
122 let provided = token_digest_sha256(
123 provided_secret
124 .map(|s| s.expose_secret())
125 .unwrap_or_default(),
126 token_hmac_key,
127 );
128 if !secret.constant_eq(&provided) {
129 return Err(ClientAuthError::ClientSecretMismatch);
130 }
131
132 Ok(client)
133}