/ backend / src / api / oauth / authorize.rs
authorize.rs
  1  use crate::api::common::response::{ErrMsg, ResponseStatus};
  2  use crate::api::core::login::rocket_uri_macro_get_login_page;
  3  use crate::models::oauth_scope::{OauthScope, OauthScopes};
  4  use crate::{
  5      api::auth::session_guard::Session,
  6      db::DB,
  7      models::oauth_scope::ScopeField,
  8      services::oauth_authorization_service::{self, Oauth2Error},
  9  };
 10  use mobc_redis::RedisConnectionManager;
 11  use rocket::{http::Status, response::Redirect, State};
 12  use rocket_dyn_templates::Template;
 13  use serde::Serialize;
 14  use sqlx::Pool;
 15  
 16  const CONSENT_TEMPLATE_NAME: &str = "oauth_consent";
 17  const RESPONSE_TYPE_CODE: &str = "code";
 18  
 19  #[derive(Serialize)]
 20  pub struct ConsentTemplateFields {
 21      client_name: String,
 22      scope_fields: Vec<ScopeField>,
 23      scope: String,
 24      client_id: String,
 25      state: String,
 26      response_type: String,
 27      redirect_uri: String,
 28  }
 29  
 30  #[derive(Responder)]
 31  pub enum GetAuthorizationResponse {
 32      #[response(status = 200)]
 33      Consent(Template),
 34      LoginRequired(Redirect),
 35      Failure(ResponseStatus<()>),
 36      Success(Redirect),
 37  }
 38  
 39  // TODO: Perhaps support nonce.
 40  /// First step in the oauth2 authorization flow.
 41  #[get("/authorize?<response_type>&<client_id>&<redirect_uri>&<state>&<scope>")]
 42  #[allow(clippy::too_many_arguments)]
 43  pub async fn get_authorization(
 44      db_pool: &State<Pool<DB>>,
 45      redis_pool: &State<mobc::Pool<RedisConnectionManager>>,
 46      response_type: String,
 47      client_id: String,
 48      redirect_uri: String,
 49      state: String,
 50      scope: Option<String>,
 51      session: Option<Session>,
 52  ) -> GetAuthorizationResponse {
 53      // TODO: I think we already support more than the response type code, look into this more.
 54      if response_type != RESPONSE_TYPE_CODE {
 55          return GetAuthorizationResponse::Failure(ResponseStatus::err(
 56              Status::UnprocessableEntity,
 57              ErrMsg::InvalidResponseType,
 58          ));
 59      }
 60  
 61      let requested_scopes = match OauthScopes::parse_or_default(&scope) {
 62          Ok(s) => s.scopes,
 63          Err(err) => {
 64              log::warn!("Failed to parse requested scopes, err: {err:?}");
 65              return GetAuthorizationResponse::Failure(ResponseStatus::err(
 66                  Status::UnprocessableEntity,
 67                  ErrMsg::InvalidScope,
 68              ));
 69          }
 70      };
 71  
 72      // If the user is not currently logged in, redirect them to the login page then returning to this endpoint.
 73      let session = match session {
 74          Some(s) => s,
 75          None => {
 76              let return_to = format!(
 77                  "/api/oauth/{}",
 78                  uri!(get_authorization(
 79                      response_type,
 80                      client_id,
 81                      redirect_uri,
 82                      state,
 83                      scope,
 84                  ))
 85                  .to_string()
 86              );
 87  
 88              let login_uri = format!(
 89                  "/api/core/{}",
 90                  uri!(get_login_page(Some(return_to))).to_string()
 91              );
 92  
 93              return GetAuthorizationResponse::LoginRequired(Redirect::found(login_uri));
 94          }
 95      };
 96  
 97      let url = match oauth_authorization_service::get_auth_token(
 98          db_pool,
 99          redis_pool,
100          client_id.clone(),
101          &redirect_uri,
102          &state,
103          session.account_id,
104          &requested_scopes,
105      )
106      .await
107      {
108          Ok(url) => url,
109          Err(Oauth2Error::ScopeNotRegistered) => {
110              error!("The client was not registered for one or more of the requested scopes");
111              return GetAuthorizationResponse::Failure(ResponseStatus::err(
112                  Status::BadRequest,
113                  ErrMsg::InvalidScope,
114              ));
115          }
116          Err(Oauth2Error::NoClientWithId) => {
117              error!("No client with id '{}'", client_id);
118              return GetAuthorizationResponse::Failure(ResponseStatus::err(
119                  Status::BadRequest,
120                  ErrMsg::InvalidClientId,
121              ));
122          }
123          Err(Oauth2Error::InvalidRedirectUri) => {
124              return GetAuthorizationResponse::Failure(ResponseStatus::err(
125                  Status::BadRequest,
126                  ErrMsg::InvalidRedirectUri,
127              ))
128          }
129          Err(Oauth2Error::MissingClientConsent { client_name }) => {
130              let scope_str = requested_scopes
131                  .iter()
132                  .map(|s| s.to_string())
133                  .collect::<Vec<String>>()
134                  .join(" ");
135  
136              let mut sorted_scopes: Vec<OauthScope> = requested_scopes.into_iter().collect();
137              sorted_scopes.sort();
138  
139              let template_fields = ConsentTemplateFields {
140                  client_name,
141                  scope_fields: sorted_scopes
142                      .into_iter()
143                      .map(|s| s.get_scope_field())
144                      .collect(),
145                  scope: scope_str,
146                  client_id,
147                  state,
148                  response_type,
149                  redirect_uri,
150              };
151  
152              return GetAuthorizationResponse::Consent(Template::render(
153                  CONSENT_TEMPLATE_NAME,
154                  template_fields,
155              ));
156          }
157          Err(err) => {
158              log::error!("An unexpected oauth2 error occurred, err: {err:?}");
159              return GetAuthorizationResponse::Failure(ResponseStatus::internal_err());
160          }
161      };
162  
163      GetAuthorizationResponse::Success(Redirect::found(url))
164  }