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_error, oauth_invalid_client,
5 ok_json_no_cache, 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;
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 refresh_ttl = Duration::days(30);
125
126 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
127 let client = authenticate_oauth_client(
128 &mut conn,
129 &form.client_id,
130 form.client_secret.as_ref(),
131 token_hmac_key,
132 )
133 .await
134 .map_err(|e| match e {
135 ClientAuthError::UnknownClient => oauth_invalid_client("invalid client_id"),
136 ClientAuthError::ClientSecretMissing => {
137 oauth_invalid_client("client_secret required for confidential clients")
138 }
139 ClientAuthError::ClientSecretMismatch => oauth_invalid_client("invalid client secret"),
140 })?;
141
142 tracing::Span::current().record("client_id", &form.client_id);
144 let access_ttl = client.access_token_ttl();
145
146 let grant_kind = form.grant.kind();
148 tracing::Span::current().record("grant_type", format!("{:?}", grant_kind));
149 if !client.allows_grant(grant_kind) {
150 return Err(oauth_error(
151 OAuthErrorCode::UnsupportedGrantType,
152 "grant type not allowed for this client",
153 None,
154 None,
155 ));
156 }
157
158 let dpop_jkt_opt = if req.headers().get("DPoP").is_some() {
160 Some(
161 verify_dpop_from_actix_for_token(
162 &mut conn,
163 &req,
164 &app_conf.oauth_server_configuration.dpop_nonce_key,
165 )
166 .await?,
167 )
168 } else {
169 if !client.bearer_allowed {
170 return Err(oauth_invalid_client(
171 "client not allowed to use other than dpop-bound tokens",
172 ));
173 }
174 None
175 };
176
177 let issued_token_type = if dpop_jkt_opt.is_some() {
178 TokenType::DPoP
179 } else {
180 TokenType::Bearer
181 };
182 tracing::Span::current().record("token_type", format!("{:?}", issued_token_type));
183
184 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
185 let token_pair = generate_token_pair(token_hmac_key);
186 let access_token = token_pair.access_token.clone();
187 let refresh_token = token_pair.refresh_token.clone();
188 let refresh_token_expires_at = Utc::now() + refresh_ttl;
189 let access_expires_at = Utc::now() + access_ttl;
190
191 let request = TokenGrantRequest {
192 grant: &form.grant,
193 client: &client,
194 token_pair,
195 access_expires_at,
196 refresh_expires_at: refresh_token_expires_at,
197 issued_token_type,
198 dpop_jkt: dpop_jkt_opt.as_deref(),
199 token_hmac_key,
200 resource: form.resource.as_ref(),
201 };
202
203 let TokenGrantResult {
204 user_id,
205 scopes: scope_vec,
206 nonce: nonce_opt,
207 access_expires_at: at_expires_at,
208 issue_id_token,
209 } = process_token_grant(&mut conn, &cache, request)
210 .await
211 .map_err(|e: TokenGrantError| ControllerError::from(e))?;
212
213 let id_token = if issue_id_token && scope_has_openid(&scope_vec) {
214 Some(generate_id_token(
215 user_id,
216 &client.client_id,
217 nonce_opt.as_deref(),
218 at_expires_at,
219 &app_conf.oauth_issuer(),
220 &app_conf,
221 )?)
222 } else {
223 None
224 };
225
226 let response = TokenResponse {
227 access_token,
228 refresh_token: Some(refresh_token),
229 id_token,
230 token_type: match issued_token_type {
231 TokenType::Bearer => "Bearer".to_string(),
232 TokenType::DPoP => "DPoP".to_string(),
233 },
234 expires_in: access_ttl.num_seconds() as u32,
235 scope: scope_vec.join(" "),
236 };
237
238 server_token.authorized_ok(ok_json_no_cache(response))
239}
240
241pub fn _add_routes(cfg: &mut web::ServiceConfig) {
242 cfg.service(
243 web::resource("/token")
244 .wrap(RateLimit::new(RateLimitConfig {
245 per_minute: Some(100),
246 per_hour: Some(500),
247 per_day: Some(2000),
248 per_month: None,
249 ..Default::default()
250 }))
251 .route(web::post().to(token)),
252 );
253}