headless_lms_server/controllers/main_frontend/oauth/
token.rs1use crate::domain::oauth::dpop::verify_dpop_from_actix;
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::build_rate_limiting_middleware;
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;
21use std::time::Duration as StdDuration;
22
23#[instrument(skip(pool, app_conf, form))]
91pub async fn token(
92 pool: web::Data<PgPool>,
93 OAuthValidated(form): OAuthValidated<TokenQuery>,
94 req: actix_web::HttpRequest,
95 app_conf: web::Data<ApplicationConfiguration>,
96) -> ControllerResult<HttpResponse> {
97 let mut conn = pool.acquire().await?;
98 let server_token = skip_authorize();
99
100 let access_ttl = Duration::hours(1);
101 let refresh_ttl = Duration::days(30);
102
103 let client = OAuthClient::find_by_client_id(&mut conn, &form.client_id)
104 .await
105 .map_err(|_| oauth_invalid_client("invalid client_id"))?;
106
107 tracing::Span::current().record("client_id", &form.client_id);
109
110 if client.is_confidential() {
111 match client.client_secret {
112 Some(ref secret) => {
113 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
114 if !secret.constant_eq(&token_digest_sha256(
115 &form.client_secret.clone().unwrap_or_default(),
116 token_hmac_key,
117 )) {
118 return Err(oauth_invalid_client("invalid client secret"));
119 }
120 }
121 None => {
122 return Err(oauth_invalid_client(
123 "client_secret required for confidential clients",
124 ));
125 }
126 }
127 }
128
129 let grant_kind = form.grant.kind();
131 tracing::Span::current().record("grant_type", format!("{:?}", grant_kind));
132 if !client.allows_grant(grant_kind) {
133 return Err(ControllerError::new(
134 ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
135 error: OAuthErrorCode::UnsupportedGrantType.as_str().into(),
136 error_description: "grant type not allowed for this client".into(),
137 redirect_uri: None,
138 state: None,
139 nonce: None,
140 })),
141 "Grant type not allowed for this client",
142 None::<anyhow::Error>,
143 ));
144 }
145
146 let dpop_jkt_opt = if req.headers().get("DPoP").is_some() {
148 Some(
149 verify_dpop_from_actix(
150 &mut conn,
151 &req,
152 "POST",
153 &app_conf.oauth_server_configuration.dpop_nonce_key,
154 None,
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.unwrap_or_default(),
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(build_rate_limiting_middleware(
234 StdDuration::from_secs(60),
235 100,
236 ))
237 .wrap(build_rate_limiting_middleware(
238 StdDuration::from_secs(60 * 60),
239 500,
240 ))
241 .wrap(build_rate_limiting_middleware(
242 StdDuration::from_secs(60 * 60 * 24),
243 2000,
244 ))
245 .route(web::post().to(token)),
246 );
247}