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::{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(|_| oauth_invalid_client("invalid client_id"))?;
105
106 tracing::Span::current().record("client_id", &form.client_id);
108
109 if client.is_confidential() {
110 match client.client_secret {
111 Some(ref secret) => {
112 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
113 if !secret.constant_eq(&token_digest_sha256(
114 &form.client_secret.clone().unwrap_or_default(),
115 token_hmac_key,
116 )) {
117 return Err(oauth_invalid_client("invalid client secret"));
118 }
119 }
120 None => {
121 return Err(oauth_invalid_client(
122 "client_secret required for confidential clients",
123 ));
124 }
125 }
126 }
127
128 let grant_kind = form.grant.kind();
130 tracing::Span::current().record("grant_type", format!("{:?}", grant_kind));
131 if !client.allows_grant(grant_kind) {
132 return Err(ControllerError::new(
133 ControllerErrorType::OAuthError(Box::new(OAuthErrorData {
134 error: OAuthErrorCode::UnsupportedGrantType.as_str().into(),
135 error_description: "grant type not allowed for this client".into(),
136 redirect_uri: None,
137 state: None,
138 nonce: None,
139 })),
140 "Grant type not allowed for this client",
141 None::<anyhow::Error>,
142 ));
143 }
144
145 let dpop_jkt_opt = if req.headers().get("DPoP").is_some() {
147 Some(
148 verify_dpop_from_actix(
149 &mut conn,
150 &req,
151 "POST",
152 &app_conf.oauth_server_configuration.dpop_nonce_key,
153 None,
154 )
155 .await?,
156 )
157 } else {
158 if !client.bearer_allowed {
159 return Err(oauth_invalid_client(
160 "client not allowed to use other than dpop-bound tokens",
161 ));
162 }
163 None
164 };
165
166 let issued_token_type = if dpop_jkt_opt.is_some() {
167 TokenType::DPoP
168 } else {
169 TokenType::Bearer
170 };
171 tracing::Span::current().record("token_type", format!("{:?}", issued_token_type));
172
173 let token_hmac_key = &app_conf.oauth_server_configuration.oauth_token_hmac_key;
174 let token_pair = generate_token_pair(token_hmac_key);
175 let access_token = token_pair.access_token.clone();
176 let refresh_token = token_pair.refresh_token.clone();
177 let refresh_token_expires_at = Utc::now() + refresh_ttl;
178 let access_expires_at = Utc::now() + access_ttl;
179
180 let request = TokenGrantRequest {
181 grant: &form.grant,
182 client: &client,
183 token_pair,
184 access_expires_at,
185 refresh_expires_at: refresh_token_expires_at,
186 issued_token_type,
187 dpop_jkt: dpop_jkt_opt.as_deref(),
188 token_hmac_key,
189 };
190
191 let TokenGrantResult {
192 user_id,
193 scopes: scope_vec,
194 nonce: nonce_opt,
195 access_expires_at: at_expires_at,
196 issue_id_token,
197 } = process_token_grant(&mut conn, request)
198 .await
199 .map_err(|e: TokenGrantError| ControllerError::from(e))?;
200
201 let base_url = app_conf.base_url.trim_end_matches('/');
202 let id_token = if issue_id_token && scope_has_openid(&scope_vec) {
203 Some(generate_id_token(
204 user_id,
205 &client.client_id,
206 &nonce_opt.unwrap_or_default(),
207 at_expires_at,
208 &format!("{}/api/v0/main-frontend/oauth", base_url),
209 &app_conf,
210 )?)
211 } else {
212 None
213 };
214
215 let response = TokenResponse {
216 access_token,
217 refresh_token: Some(refresh_token),
218 id_token,
219 token_type: match issued_token_type {
220 TokenType::Bearer => "Bearer".to_string(),
221 TokenType::DPoP => "DPoP".to_string(),
222 },
223 expires_in: access_ttl.num_seconds() as u32,
224 };
225
226 server_token.authorized_ok(ok_json_no_cache(response))
227}
228
229pub fn _add_routes(cfg: &mut web::ServiceConfig) {
230 cfg.service(
231 web::resource("/token")
232 .wrap(RateLimit::new(RateLimitConfig {
233 per_minute: Some(100),
234 per_hour: Some(500),
235 per_day: Some(2000),
236 per_month: None,
237 }))
238 .route(web::post().to(token)),
239 );
240}