From 31f80998a8eef32c0ef2d309bee68ab88f453bab Mon Sep 17 00:00:00 2001 From: mrw1593 Date: Thu, 22 Jun 2023 20:36:06 -0400 Subject: Implement the password grant --- src/api/clients.rs | 48 ++++++++++++++++++++++++++++++++--- src/api/oauth.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 5 deletions(-) (limited to 'src/api') diff --git a/src/api/clients.rs b/src/api/clients.rs index 7b6ec94..27ef995 100644 --- a/src/api/clients.rs +++ b/src/api/clients.rs @@ -7,7 +7,7 @@ use thiserror::Error; use url::Url; use uuid::Uuid; -use crate::models::client::{Client, ClientType, NoSecretError}; +use crate::models::client::{Client, ClientType, CreateClientError}; use crate::services::crypto::PasswordHash; use crate::services::db::ClientRow; use crate::services::{db, id}; @@ -20,6 +20,7 @@ struct ClientResponse { client_type: ClientType, allowed_scopes: Box<[Box]>, default_scopes: Option]>>, + is_trusted: bool, } impl From for ClientResponse { @@ -36,6 +37,7 @@ impl From for ClientResponse { default_scopes: value .default_scopes .map(|s| s.split_whitespace().map(Box::from).collect()), + is_trusted: value.is_trusted, } } } @@ -164,6 +166,21 @@ async fn get_client_default_scopes( Ok(HttpResponse::Ok().json(default_scopes)) } +#[get("/{client_id}/is-trusted")] +async fn get_client_is_trusted( + client_id: web::Path, + db: web::Data, +) -> Result { + let db = db.as_ref(); + let id = *client_id; + + let Some(is_trusted) = db::is_client_trusted(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + Ok(HttpResponse::Ok().json(is_trusted)) +} + #[derive(Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct ClientRequest { @@ -173,6 +190,7 @@ struct ClientRequest { secret: Option>, allowed_scopes: Box<[Box]>, default_scopes: Option]>>, + trusted: bool, } #[derive(Debug, Clone, Error)] @@ -216,6 +234,7 @@ async fn create_client( body.allowed_scopes.clone(), body.default_scopes.clone(), &body.redirect_uris, + body.trusted, ) .map_err(|e| e.unwrap())?; @@ -233,7 +252,7 @@ enum UpdateClientError { #[error(transparent)] NotFound(#[from] ClientNotFound), #[error(transparent)] - NoSecret(#[from] NoSecretError), + ClientError(#[from] CreateClientError), #[error(transparent)] AliasTaken(#[from] AliasTakenError), } @@ -242,7 +261,7 @@ impl ResponseError for UpdateClientError { fn status_code(&self) -> StatusCode { match self { Self::NotFound(e) => e.status_code(), - Self::NoSecret(e) => e.status_code(), + Self::ClientError(e) => e.status_code(), Self::AliasTaken(e) => e.status_code(), } } @@ -273,6 +292,7 @@ async fn update_client( body.allowed_scopes.clone(), body.default_scopes.clone(), &body.redirect_uris, + body.trusted, ) .map_err(|e| e.unwrap())?; @@ -370,6 +390,25 @@ async fn update_client_default_scopes( Ok(HttpResponse::NoContent().finish()) } +#[put("/{id}/is-trusted")] +async fn update_client_is_trusted( + id: web::Path, + body: web::Json, + db: web::Data, +) -> Result { + let db = db.get_ref(); + let id = *id; + let is_trusted = *body; + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + db::update_client_trusted(db, id, is_trusted).await.unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + #[put("/{id}/redirect-uris")] async fn update_client_redirect_uris( id: web::Path, @@ -405,7 +444,7 @@ async fn update_client_secret( }; if client_type == ClientType::Confidential && body.is_none() { - yeet!(NoSecretError::new().into()) + yeet!(CreateClientError::NoSecret.into()) } let secret = body.0.map(|s| PasswordHash::new(&s).unwrap()); @@ -422,6 +461,7 @@ pub fn service() -> Scope { .service(get_client_allowed_scopes) .service(get_client_default_scopes) .service(get_client_redirect_uris) + .service(get_client_is_trusted) .service(create_client) .service(update_client) .service(update_client_alias) diff --git a/src/api/oauth.rs b/src/api/oauth.rs index 5d1f12a..43ad402 100644 --- a/src/api/oauth.rs +++ b/src/api/oauth.rs @@ -512,6 +512,14 @@ impl TokenError { error_description: err.to_string().into_boxed_str(), } } + + fn untrusted_client() -> Self { + Self { + status_code: StatusCode::UNAUTHORIZED, + error: TokenErrorType::InvalidClient, + error_description: "Only trusted clients may use this grant".into(), + } + } } impl ResponseError for TokenError { @@ -619,7 +627,70 @@ async fn token( username, password, scope, - } => todo!(), + } => { + let Some(authorization) = authorization else { + return TokenError::no_authorization().error_response(); + }; + let client_alias = authorization.username(); + let Some(client_id) = db::get_client_id_by_alias(db, client_alias).await.unwrap() else { + return TokenError::client_not_found(client_alias).error_response(); + }; + + let trusted = db::is_client_trusted(db, client_id).await.unwrap().unwrap(); + if !trusted { + return TokenError::untrusted_client().error_response(); + } + + // verify client + let hash = db::get_client_secret(db, client_id).await.unwrap().unwrap(); + if !hash.check_password(authorization.password()).unwrap() { + return TokenError::incorrect_client_secret().error_response(); + } + + // verify scope + let allowed_scopes = db::get_client_allowed_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + let scope = if let Some(scope) = &scope { + scope.clone() + } else { + let default_scopes = db::get_client_default_scopes(db, client_id) + .await + .unwrap() + .unwrap(); + let Some(scope) = default_scopes else { + return TokenError::no_scope().error_response(); + }; + scope + }; + if !scopes::is_subset_of(&scope, &allowed_scopes) { + return TokenError::excessive_scope().error_response(); + } + + let access_token = + jwt::Claims::access_token(db, None, self_id, client_id, duration, &scope) + .await + .unwrap(); + let refresh_token = jwt::Claims::refresh_token(db, &access_token).await.unwrap(); + + let expires_in = access_token.expires_in(); + let scope = access_token.scopes().into(); + let access_token = access_token.to_jwt().unwrap(); + let refresh_token = Some(refresh_token.to_jwt().unwrap()); + + let response = TokenResponse { + access_token, + token_type, + expires_in, + refresh_token, + scope, + }; + HttpResponse::Ok() + .insert_header(cache_control) + .insert_header((header::PRAGMA, "no-cache")) + .json(response) + } GrantType::ClientCredentials { scope } => { let Some(authorization) = authorization else { return TokenError::no_authorization().error_response(); -- cgit v1.2.3