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::{
4 ClientAuthError, authenticate_oauth_client, oauth_invalid_client, ok_json_no_cache,
5 scope_has_openid,
6};
7use crate::domain::oauth::oauth_validated::OAuthValidated;
8use crate::domain::oauth::oidc::generate_id_token;
9use crate::domain::oauth::token_query::TokenQuery;
10use crate::domain::oauth::token_response::TokenResponse;
11use crate::domain::oauth::token_service::{
12 TokenGrantRequest, TokenGrantResult, generate_token_pair, process_token_grant,
13};
14use crate::domain::rate_limit_middleware_builder::{RateLimit, RateLimitConfig};
15use crate::prelude::*;
16use actix_web::{HttpResponse, web};
17use chrono::{Duration, Utc};
18use domain::error::{OAuthErrorCode, OAuthErrorData};
19use headless_lms_base::config::ApplicationConfiguration;
20use headless_lms_utils::cache::Cache;
21use models::oauth_access_token::TokenType;
22use sqlx::PgPool;
23use utoipa::OpenApi;
24
25#[derive(OpenApi)]
26#[openapi(paths(token))]
27#[allow(dead_code)]
28pub(crate) struct MainFrontendOauthTokenApiDoc;
29
30#[instrument(skip(pool, app_conf, form, cache))]
98#[utoipa::path(
99 post,
100 path = "/token",
101 operation_id = "exchangeOauthToken",
102 tag = "oauth",
103 request_body(
104 content = serde_json::Value,
105 content_type = "application/x-www-form-urlencoded"
106 ),
107 responses(
108 (status = 200, description = "OAuth token response", body = serde_json::Value),
109 (status = 401, description = "OAuth token error")
110 )
111)]
112pub async fn token(
113 pool: web::Data<PgPool>,
114 OAuthValidated(form): OAuthValidated<TokenQuery>,
115 req: actix_web::HttpRequest,
116 app_conf: web::Data<ApplicationConfiguration>,
117 cache: web::Data<Cache>,
118) -> ControllerResult<HttpResponse> {
119 let mut conn = pool.acquire().await?;
120 let server_token = skip_authorize();
121
122 let access_ttl = Duration::hours(1);
123 let refresh_ttl = Duration::days(30);
124
125 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
126 let client = authenticate_oauth_client(
127 &mut conn,
128 &form.client_id,
129 form.client_secret.as_ref(),
130 token_hmac_key,
131 )
132 .await
133 .map_err(|e| match e {
134 ClientAuthError::UnknownClient => oauth_invalid_client("invalid client_id"),
135 ClientAuthError::ClientSecretMissing => {
136 oauth_invalid_client("client_secret required for confidential clients")
137 }
138 ClientAuthError::ClientSecretMismatch => oauth_invalid_client("invalid client secret"),
139 })?;
140
141 tracing::Span::current().record("client_id", &form.client_id);
143
144 let grant_kind = form.grant.kind();
146 tracing::Span::current().record("grant_type", format!("{:?}", grant_kind));
147 if !client.allows_grant(grant_kind) {
148 return Err(ControllerError::new(
149 ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
150 error: OAuthErrorCode::UnsupportedGrantType.as_str().into(),
151 error_description: "grant type not allowed for this client".into(),
152 redirect_uri: None,
153 state: None,
154 nonce: None,
155 })),
156 "Grant type not allowed for this client",
157 None::<anyhow::Error>,
158 ));
159 }
160
161 let dpop_jkt_opt = if req.headers().get("DPoP").is_some() {
163 Some(
164 verify_dpop_from_actix_for_token(
165 &mut conn,
166 &req,
167 &app_conf.oauth_server_configuration.dpop_nonce_key,
168 )
169 .await?,
170 )
171 } else {
172 if !client.bearer_allowed {
173 return Err(oauth_invalid_client(
174 "client not allowed to use other than dpop-bound tokens",
175 ));
176 }
177 None
178 };
179
180 let issued_token_type = if dpop_jkt_opt.is_some() {
181 TokenType::DPoP
182 } else {
183 TokenType::Bearer
184 };
185 tracing::Span::current().record("token_type", format!("{:?}", issued_token_type));
186
187 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
188 let token_pair = generate_token_pair(token_hmac_key);
189 let access_token = token_pair.access_token.clone();
190 let refresh_token = token_pair.refresh_token.clone();
191 let refresh_token_expires_at = Utc::now() + refresh_ttl;
192 let access_expires_at = Utc::now() + access_ttl;
193
194 let request = TokenGrantRequest {
195 grant: &form.grant,
196 client: &client,
197 token_pair,
198 access_expires_at,
199 refresh_expires_at: refresh_token_expires_at,
200 issued_token_type,
201 dpop_jkt: dpop_jkt_opt.as_deref(),
202 token_hmac_key,
203 };
204
205 let TokenGrantResult {
206 user_id,
207 scopes: scope_vec,
208 nonce: nonce_opt,
209 access_expires_at: at_expires_at,
210 issue_id_token,
211 } = process_token_grant(&mut conn, &cache, request)
212 .await
213 .map_err(|e: TokenGrantError| ControllerError::from(e))?;
214
215 let base_url = app_conf.base_url.trim_end_matches('/');
216 let id_token = if issue_id_token && scope_has_openid(&scope_vec) {
217 Some(generate_id_token(
218 user_id,
219 &client.client_id,
220 nonce_opt.as_deref(),
221 at_expires_at,
222 &format!("{}/api/v0/main-frontend/oauth", base_url),
223 &app_conf,
224 )?)
225 } else {
226 None
227 };
228
229 let response = TokenResponse {
230 access_token,
231 refresh_token: Some(refresh_token),
232 id_token,
233 token_type: match issued_token_type {
234 TokenType::Bearer => "Bearer".to_string(),
235 TokenType::DPoP => "DPoP".to_string(),
236 },
237 expires_in: access_ttl.num_seconds() as u32,
238 };
239
240 server_token.authorized_ok(ok_json_no_cache(response))
241}
242
243pub fn _add_routes(cfg: &mut web::ServiceConfig) {
244 cfg.service(
245 web::resource("/token")
246 .wrap(RateLimit::new(RateLimitConfig {
247 per_minute: Some(100),
248 per_hour: Some(500),
249 per_day: Some(2000),
250 per_month: None,
251 ..Default::default()
252 }))
253 .route(web::post().to(token)),
254 );
255}