From ac7317226405fc90e8439a0c1bef91cecd539d02 Mon Sep 17 00:00:00 2001 From: mrw1593 Date: Sun, 11 Jun 2023 15:34:00 -0400 Subject: Implement the authorization code grant --- src/services/db/client.rs | 44 +++++++++++++++++++++++++++++++++++++- src/services/jwt.rs | 54 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 80 insertions(+), 18 deletions(-) (limited to 'src/services') diff --git a/src/services/db/client.rs b/src/services/db/client.rs index c25ad0d..70701d7 100644 --- a/src/services/db/client.rs +++ b/src/services/db/client.rs @@ -21,6 +21,13 @@ pub struct ClientRow { pub default_scopes: Option, } +#[derive(Clone, FromRow)] +struct HashRow { + secret_hash: Option>, + secret_salt: Option>, + secret_version: Option, +} + pub async fn client_id_exists<'c>( executor: impl Executor<'c, Database = MySql>, id: Uuid, @@ -47,6 +54,19 @@ pub async fn client_alias_exists<'c>( .unexpect() } +pub async fn get_client_id_by_alias<'c>( + executor: impl Executor<'c, Database = MySql>, + alias: &str, +) -> Result, RawUnexpected> { + query_scalar!( + "SELECT id as `id: Uuid` FROM clients WHERE alias = ?", + alias + ) + .fetch_optional(executor) + .await + .unexpect() +} + pub async fn get_client_response<'c>( executor: impl Executor<'c, Database = MySql>, id: Uuid, @@ -116,6 +136,28 @@ pub async fn get_client_default_scopes<'c>( Ok(scopes.map(|s| s.map(Box::from))) } +pub async fn get_client_secret<'c>( + executor: impl Executor<'c, Database = MySql>, + id: Uuid, +) -> Result, RawUnexpected> { + let hash = query_as!( + HashRow, + r"SELECT secret_hash, secret_salt, secret_version + FROM clients WHERE id = ?", + id + ) + .fetch_optional(executor) + .await?; + + let Some(hash) = hash else { return Ok(None) }; + let Some(version) = hash.secret_version else { return Ok(None) }; + let Some(salt) = hash.secret_hash else { return Ok(None) }; + let Some(hash) = hash.secret_salt else { return Ok(None) }; + + let hash = PasswordHash::from_fields(&hash, &salt, version as u8); + Ok(Some(hash)) +} + pub async fn get_client_redirect_uris<'c>( executor: impl Executor<'c, Database = MySql>, id: Uuid, @@ -136,7 +178,7 @@ pub async fn get_client_redirect_uris<'c>( pub async fn client_has_redirect_uri<'c>( executor: impl Executor<'c, Database = MySql>, id: Uuid, - url: Url, + url: &Url, ) -> Result { query_scalar!( r"SELECT EXISTS( diff --git a/src/services/jwt.rs b/src/services/jwt.rs index 7841afb..822101f 100644 --- a/src/services/jwt.rs +++ b/src/services/jwt.rs @@ -32,6 +32,7 @@ pub struct Claims { client_id: Uuid, auth_code_id: Uuid, token_type: TokenType, + redirect_uri: Option, } #[derive(Debug, Clone, Copy, sqlx::Type)] @@ -43,18 +44,19 @@ pub enum RevokedRefreshTokenReason { impl Claims { pub async fn auth_code<'c>( - db: MySqlPool, + db: &MySqlPool, self_id: Url, client_id: Uuid, scopes: &str, + redirect_uri: &Url, ) -> Result { let five_minutes = Duration::minutes(5); - let id = new_id(&db, db::auth_code_exists).await?; + let id = new_id(db, db::auth_code_exists).await?; let time = Utc::now(); let exp = time + five_minutes; - db::create_auth_code(&db, id, exp).await?; + db::create_auth_code(db, id, exp).await?; Ok(Self { iss: self_id, @@ -67,22 +69,23 @@ impl Claims { client_id, auth_code_id: id, token_type: TokenType::Authorization, + redirect_uri: Some(redirect_uri.clone()), }) } pub async fn access_token<'c>( - db: MySqlPool, + db: &MySqlPool, auth_code_id: Uuid, self_id: Url, client_id: Uuid, duration: Duration, scopes: &str, ) -> Result { - let id = new_id(&db, db::access_token_exists).await?; + let id = new_id(db, db::access_token_exists).await?; let time = Utc::now(); let exp = time + duration; - db::create_access_token(&db, id, auth_code_id, exp) + db::create_access_token(db, id, auth_code_id, exp) .await .unexpect()?; @@ -97,19 +100,23 @@ impl Claims { client_id, auth_code_id, token_type: TokenType::Access, + redirect_uri: None, }) } - pub async fn refresh_token(db: MySqlPool, other_token: Claims) -> Result { + pub async fn refresh_token( + db: &MySqlPool, + other_token: &Claims, + ) -> Result { let one_day = Duration::days(1); - let id = new_id(&db, db::refresh_token_exists).await?; + let id = new_id(db, db::refresh_token_exists).await?; let time = Utc::now(); let exp = other_token.exp + one_day; - db::create_refresh_token(&db, id, other_token.auth_code_id, exp).await?; + db::create_refresh_token(db, id, other_token.auth_code_id, exp).await?; - let mut claims = other_token; + let mut claims = other_token.clone(); claims.exp = exp; claims.iat = Some(time); claims.jti = id; @@ -119,15 +126,15 @@ impl Claims { } pub async fn refreshed_access_token( - db: MySqlPool, + db: &MySqlPool, refresh_token: Claims, exp_time: Duration, ) -> Result { - let id = new_id(&db, db::access_token_exists).await?; + let id = new_id(db, db::access_token_exists).await?; let time = Utc::now(); let exp = time + exp_time; - db::create_access_token(&db, id, refresh_token.auth_code_id, exp).await?; + db::create_access_token(db, id, refresh_token.auth_code_id, exp).await?; let mut claims = refresh_token; claims.exp = exp; @@ -142,6 +149,10 @@ impl Claims { self.jti } + pub fn expires_in(&self) -> i64 { + (self.exp - Utc::now()).num_seconds() + } + pub fn scopes(&self) -> &str { &self.scope } @@ -163,6 +174,8 @@ pub enum VerifyJwtError { WrongClient, #[error("The given audience parameter does not contain this issuer")] BadAudience, + #[error("The redirect URI doesn't match what's in the token")] + IncorrectRedirectUri, #[error("The token is expired")] ExpiredToken, #[error("The token cannot be used yet")] @@ -211,16 +224,23 @@ fn verify_jwt( } pub async fn verify_auth_code<'c>( - db: MySqlPool, + db: &MySqlPool, token: &str, self_id: Url, client_id: Uuid, + redirect_uri: Url, ) -> Result> { let claims = verify_jwt(token, self_id, client_id)?; - if db::delete_auth_code(&db, claims.jti).await? { - db::delete_access_tokens_with_auth_code(&db, claims.jti).await?; - db::revoke_refresh_tokens_with_auth_code(&db, claims.jti).await?; + if let Some(claimed_uri) = &claims.redirect_uri { + if claimed_uri.clone() != redirect_uri { + yeet!(VerifyJwtError::IncorrectRedirectUri.into()); + } + } + + if db::delete_auth_code(db, claims.jti).await? { + db::delete_access_tokens_with_auth_code(db, claims.jti).await?; + db::revoke_refresh_tokens_with_auth_code(db, claims.jti).await?; yeet!(VerifyJwtError::JwtRevoked.into()); } -- cgit v1.2.3