diff --git a/CHANGELOG.md b/CHANGELOG.md index b92fe06..f1cdfcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added `/api/v1/apps` endpoint. - Added OAuth authorization page. +- Support `authorization_code` OAuth grant type. - Documented `http_cors_allowlist` configuration parameter. ### Changed diff --git a/docs/openapi.yaml b/docs/openapi.yaml index af3219b..d06cdce 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -18,9 +18,15 @@ paths: grant_type: type: string enum: + - authorization_code - password - eip4361 example: eip4361 + code: + description: A user authorization code, obtained via GET /oauth/authorize (required if grant type is "authorization_code"). + type: string + nullable: true + example: null username: description: User name (required if grant type is "password"). type: string diff --git a/src/mastodon_api/oauth/types.rs b/src/mastodon_api/oauth/types.rs index c7c236d..6daabfc 100644 --- a/src/mastodon_api/oauth/types.rs +++ b/src/mastodon_api/oauth/types.rs @@ -18,6 +18,11 @@ pub struct AuthorizationQueryParams { #[derive(Deserialize)] pub struct TokenRequest { pub grant_type: String, + + // Required if grant type is "authorization_code" + pub code: Option, + + // Required if grant type is "password" or "eip4361" pub username: Option, pub wallet_address: Option, // Required only with "password" and "ethereum" grant types diff --git a/src/mastodon_api/oauth/views.rs b/src/mastodon_api/oauth/views.rs index 5049e13..24adf9d 100644 --- a/src/mastodon_api/oauth/views.rs +++ b/src/mastodon_api/oauth/views.rs @@ -2,6 +2,7 @@ use actix_web::{ get, post, web, + Either, HttpResponse, Scope as ActixScope, http::header as http_header, @@ -17,6 +18,7 @@ use crate::models::oauth::queries::{ create_oauth_authorization, delete_oauth_token, get_oauth_app_by_client_id, + get_user_by_authorization_code, save_oauth_token, }; use crate::models::users::queries::{ @@ -108,10 +110,25 @@ const ACCESS_TOKEN_EXPIRES_IN: i64 = 86400 * 7; async fn token_view( config: web::Data, db_pool: web::Data, - request_data: web::Json, + request_data: Either< + web::Json, + web::Form, + >, ) -> Result { + let request_data = match request_data { + Either::Left(json) => json.into_inner(), + Either::Right(form) => form.into_inner(), + }; let db_client = &**get_database_client(&db_pool).await?; let user = match request_data.grant_type.as_str() { + "authorization_code" => { + let authorization_code = request_data.code.as_ref() + .ok_or(ValidationError("authorization code is required"))?; + get_user_by_authorization_code( + db_client, + authorization_code, + ).await? + }, "password" => { let username = request_data.username.as_ref() .ok_or(ValidationError("username is required"))?; diff --git a/src/models/oauth/queries.rs b/src/models/oauth/queries.rs index e7eca32..578c2c7 100644 --- a/src/models/oauth/queries.rs +++ b/src/models/oauth/queries.rs @@ -90,6 +90,29 @@ pub async fn create_oauth_authorization( Ok(()) } +pub async fn get_user_by_authorization_code( + db_client: &impl DatabaseClient, + authorization_code: &str, +) -> Result { + let maybe_row = db_client.query_opt( + " + SELECT user_account, actor_profile + FROM oauth_authorization + JOIN user_account ON oauth_authorization.user_id = user_account.id + JOIN actor_profile ON user_account.id = actor_profile.id + WHERE + oauth_authorization.code = $1 + AND oauth_authorization.expires_at > CURRENT_TIMESTAMP + ", + &[&authorization_code], + ).await?; + let row = maybe_row.ok_or(DatabaseError::NotFound("authorization"))?; + let db_user: DbUser = row.try_get("user_account")?; + let db_profile: DbActorProfile = row.try_get("actor_profile")?; + let user = User::new(db_user, db_profile); + Ok(user) +} + pub async fn save_oauth_token( db_client: &impl DatabaseClient, owner_id: &Uuid,