headless_lms_server/controllers/main_frontend/oauth/
token.rs1use crate::domain::oauth::dpop::verify_dpop_from_actix_for_token;
2use crate::domain::oauth::errors::TokenGrantError;
3use crate::domain::oauth::helpers::{oauth_invalid_client, ok_json_no_cache, scope_has_openid};
4use crate::domain::oauth::oauth_validated::OAuthValidated;
5use crate::domain::oauth::oidc::generate_id_token;
6use crate::domain::oauth::token_query::TokenQuery;
7use crate::domain::oauth::token_response::TokenResponse;
8use crate::domain::oauth::token_service::{
9 TokenGrantRequest, TokenGrantResult, generate_token_pair, process_token_grant,
10};
11use crate::domain::rate_limit_middleware_builder::{RateLimit, RateLimitConfig};
12use crate::prelude::*;
13use actix_web::{HttpResponse, web};
14use chrono::{Duration, Utc};
15use domain::error::{OAuthErrorCode, OAuthErrorData};
16use headless_lms_utils::ApplicationConfiguration;
17use models::{
18 library::oauth::token_digest_sha256, oauth_access_token::TokenType, oauth_client::OAuthClient,
19};
20use sqlx::PgPool;
21
22#[instrument(skip(pool, app_conf, form))]
90pub async fn token(
91 pool: web::Data<PgPool>,
92 OAuthValidated(form): OAuthValidated<TokenQuery>,
93 req: actix_web::HttpRequest,
94 app_conf: web::Data<ApplicationConfiguration>,
95) -> ControllerResult<HttpResponse> {
96 let mut conn = pool.acquire().await?;
97 let server_token = skip_authorize();
98
99 let access_ttl = Duration::hours(1);
100 let refresh_ttl = Duration::days(30);
101
102 let client = OAuthClient::find_by_client_id(&mut conn, &form.client_id)
103 .await
104 .map_err(|e| {
105 tracing::error!(err = %e, "OAuth token: client lookup failed");
106 oauth_invalid_client("invalid client_id")
107 })?;
108
109 tracing::Span::current().record("client_id", &form.client_id);
111
112 if client.is_confidential() {
113 match client.client_secret {
114 Some(ref secret) => {
115 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
116 if !secret.constant_eq(&token_digest_sha256(
117 &form.client_secret.clone().unwrap_or_default(),
118 token_hmac_key,
119 )) {
120 return Err(oauth_invalid_client("invalid client secret"));
121 }
122 }
123 None => {
124 return Err(oauth_invalid_client(
125 "client_secret required for confidential clients",
126 ));
127 }
128 }
129 }
130
131 let grant_kind = form.grant.kind();
133 tracing::Span::current().record("grant_type", format!("{:?}", grant_kind));
134 if !client.allows_grant(grant_kind) {
135 return Err(ControllerError::new(
136 ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
137 error: OAuthErrorCode::UnsupportedGrantType.as_str().into(),
138 error_description: "grant type not allowed for this client".into(),
139 redirect_uri: None,
140 state: None,
141 nonce: None,
142 })),
143 "Grant type not allowed for this client",
144 None::<anyhow::Error>,
145 ));
146 }
147
148 let dpop_jkt_opt = if req.headers().get("DPoP").is_some() {
150 Some(
151 verify_dpop_from_actix_for_token(
152 &mut conn,
153 &req,
154 &app_conf.oauth_server_configuration.dpop_nonce_key,
155 )
156 .await?,
157 )
158 } else {
159 if !client.bearer_allowed {
160 return Err(oauth_invalid_client(
161 "client not allowed to use other than dpop-bound tokens",
162 ));
163 }
164 None
165 };
166
167 let issued_token_type = if dpop_jkt_opt.is_some() {
168 TokenType::DPoP
169 } else {
170 TokenType::Bearer
171 };
172 tracing::Span::current().record("token_type", format!("{:?}", issued_token_type));
173
174 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
175 let token_pair = generate_token_pair(token_hmac_key);
176 let access_token = token_pair.access_token.clone();
177 let refresh_token = token_pair.refresh_token.clone();
178 let refresh_token_expires_at = Utc::now() + refresh_ttl;
179 let access_expires_at = Utc::now() + access_ttl;
180
181 let request = TokenGrantRequest {
182 grant: &form.grant,
183 client: &client,
184 token_pair,
185 access_expires_at,
186 refresh_expires_at: refresh_token_expires_at,
187 issued_token_type,
188 dpop_jkt: dpop_jkt_opt.as_deref(),
189 token_hmac_key,
190 };
191
192 let TokenGrantResult {
193 user_id,
194 scopes: scope_vec,
195 nonce: nonce_opt,
196 access_expires_at: at_expires_at,
197 issue_id_token,
198 } = process_token_grant(&mut conn, request)
199 .await
200 .map_err(|e: TokenGrantError| ControllerError::from(e))?;
201
202 let base_url = app_conf.base_url.trim_end_matches('/');
203 let id_token = if issue_id_token && scope_has_openid(&scope_vec) {
204 Some(generate_id_token(
205 user_id,
206 &client.client_id,
207 nonce_opt.as_deref(),
208 at_expires_at,
209 &format!("{}/api/v0/main-frontend/oauth", base_url),
210 &app_conf,
211 )?)
212 } else {
213 None
214 };
215
216 let response = TokenResponse {
217 access_token,
218 refresh_token: Some(refresh_token),
219 id_token,
220 token_type: match issued_token_type {
221 TokenType::Bearer => "Bearer".to_string(),
222 TokenType::DPoP => "DPoP".to_string(),
223 },
224 expires_in: access_ttl.num_seconds() as u32,
225 };
226
227 server_token.authorized_ok(ok_json_no_cache(response))
228}
229
230pub fn _add_routes(cfg: &mut web::ServiceConfig) {
231 cfg.service(
232 web::resource("/token")
233 .wrap(RateLimit::new(RateLimitConfig {
234 per_minute: Some(100),
235 per_hour: Some(500),
236 per_day: Some(2000),
237 per_month: None,
238 }))
239 .route(web::post().to(token)),
240 );
241}