headless_lms_server/controllers/main_frontend/oauth/
consent.rs1use crate::domain::oauth::consent_deny_query::ConsentDenyQuery;
2use crate::domain::oauth::consent_query::ConsentQuery;
3use crate::domain::oauth::consent_response::ConsentResponse;
4use crate::domain::oauth::helpers::oauth_invalid_request;
5use crate::prelude::*;
6use actix_web::{Error, HttpResponse, web};
7use models::{oauth_client::OAuthClient, oauth_user_client_scopes::OAuthUserClientScopes};
8use sqlx::PgPool;
9use url::{Url, form_urlencoded};
10
11#[instrument(skip(pool))]
31pub async fn approve_consent(
32 pool: web::Data<PgPool>,
33 form: web::Json<ConsentQuery>,
34 user: AuthUser,
35) -> ControllerResult<HttpResponse> {
36 let mut conn = pool.acquire().await?;
37 let token = skip_authorize();
38
39 let client = OAuthClient::find_by_client_id(&mut conn, &form.client_id).await?;
40 if !client.redirect_uris.contains(&form.redirect_uri) {
41 return Err(oauth_invalid_request(
42 "redirect_uri does not match client",
43 None, Some(&form.state),
45 ));
46 }
47
48 let requested_scopes: Vec<String> = form
50 .scope
51 .split_whitespace()
52 .map(|s| s.to_string())
53 .collect();
54
55 let allowed_scopes = &client.scopes;
56
57 for scope in &requested_scopes {
58 if !allowed_scopes.contains(scope) {
59 return Err(oauth_invalid_request(
60 "invalid scope",
61 None,
62 Some(&form.state),
63 ));
64 }
65 }
66
67 OAuthUserClientScopes::insert(&mut conn, user.id, client.id, &requested_scopes).await?;
68
69 let mut query_string = String::new();
71 {
72 let mut query_builder = form_urlencoded::Serializer::new(&mut query_string);
73 query_builder
74 .append_pair("client_id", &form.client_id)
75 .append_pair("redirect_uri", &form.redirect_uri)
76 .append_pair("scope", &form.scope)
77 .append_pair("state", &form.state)
78 .append_pair("nonce", &form.nonce)
79 .append_pair("response_type", &form.response_type);
80
81 if let Some(code_challenge) = &form.code_challenge {
83 query_builder.append_pair("code_challenge", code_challenge);
84 }
85 if let Some(code_challenge_method) = &form.code_challenge_method {
86 query_builder.append_pair("code_challenge_method", code_challenge_method);
87 }
88 }
89 let query = query_string;
90
91 let location = format!("/api/v0/main-frontend/oauth/authorize?{}", query);
93
94 token.authorized_ok(HttpResponse::Ok().json(ConsentResponse {
95 redirect_uri: location,
96 }))
97}
98
99#[instrument]
116pub async fn deny_consent(
117 pool: web::Data<PgPool>,
118 form: web::Json<ConsentDenyQuery>,
119) -> Result<HttpResponse, Error> {
120 let mut conn = pool
121 .acquire()
122 .await
123 .map_err(actix_web::error::ErrorInternalServerError)?;
124
125 let client = OAuthClient::find_by_client_id(&mut conn, &form.client_id)
126 .await
127 .map_err(actix_web::error::ErrorBadRequest)?;
128
129 if !client.redirect_uris.contains(&form.redirect_uri) {
130 return Err(actix_web::error::ErrorBadRequest("invalid redirect URI"));
131 }
132
133 let mut url = Url::parse(&form.redirect_uri)
134 .map_err(|_| actix_web::error::ErrorBadRequest("invalid redirect URI"))?;
135
136 {
137 let mut qp = url.query_pairs_mut();
138 qp.append_pair("error", "access_denied");
139 if !form.state.is_empty() {
140 qp.append_pair("state", &form.state);
141 }
142 }
143
144 Ok(HttpResponse::Found()
145 .append_header(("Location", url.to_string()))
146 .finish())
147}
148
149pub fn _add_routes(cfg: &mut web::ServiceConfig) {
150 cfg.route("/consent", web::post().to(approve_consent))
151 .route("/consent/deny", web::post().to(deny_consent));
152}