feat: OAuth authz callback service to fetch && save OAuth access token
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Aravinth Manivannan 2024-05-08 15:02:02 +05:30
parent 8fb056a669
commit 36fd8a3f88
Signed by: realaravinth
GPG key ID: F8F50389936984FF
5 changed files with 250 additions and 0 deletions

View file

@ -0,0 +1,17 @@
//pub mod errors;
pub mod process_authorization_response;
pub mod request_authorization;
// ## 1. Request authorization
// 1. Generate state
// 2. Save state
// 3. Redirect user to authorization page on forge:
// Gitea: https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE
// GitHub: https://github.com/login/oauth/authorize?client_id=12345&state=abcdefg
// ## 2. Process authorization response
// 1. Get state code. If not present -> error
// 2. Get access code against `code`
// 3. Save access code, refresh_code and expires_in
// ## 3. Refresh access toekn
// 1. Get access code against `refresh_code`
// 2. Save access code, refresh_code and expires_in

View file

@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
use url::Url;
use super::errors::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProcessAuthorizationResponseCommand {
redirect_uri: Option<Url>,
state: String,
code: String,
oauth_provider: String,
}
impl ProcessAuthorizationResponseCommand {
pub fn redirect_uri(&self) -> Option<&Url> {
self.redirect_uri.as_ref()
}
pub fn state(&self) -> &str {
&self.state
}
pub fn oauth_provider(&self) -> &str {
&self.oauth_provider
}
pub fn code(&self) -> &str {
&self.code
}
pub fn new_command(
redirect_uri: Option<Url>,
state: String,
code: String,
oauth_provider: String,
) -> ProcessAuthorizationServiceResult<Self> {
let state = state.trim().to_string();
let oauth_provider = oauth_provider.trim().to_string();
let code = code.trim().to_string();
Ok(Self {
state,
code,
redirect_uri,
oauth_provider,
})
}
}

View file

@ -0,0 +1,33 @@
use derive_more::Display;
use serde::{Deserialize, Serialize};
use crate::auth::application::port::out::db::errors::OutDBPortError;
use crate::auth::application::port::out::forge::errors::OutForgePortError;
pub type ProcessAuthorizationServiceResult<V> = Result<V, ProcessAuthorizationServiceError>;
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum ProcessAuthorizationServiceError {
InteralError,
BadRequest,
}
impl From<OutDBPortError> for ProcessAuthorizationServiceError {
fn from(v: OutDBPortError) -> Self {
match v {
OutDBPortError::DuplicateState => Self::InteralError, // only happens when there's a
// bug in code
OutDBPortError::InternalError => Self::InteralError,
OutDBPortError::DuplicateAccessToken => Self::InteralError, // only happens when bug in
// code
}
}
}
impl From<OutForgePortError> for ProcessAuthorizationServiceError {
fn from(v: OutForgePortError) -> Self {
match v {
OutForgePortError::InteralError => Self::InteralError,
}
}
}

View file

@ -0,0 +1,11 @@
mod command;
pub mod errors;
mod service;
#[async_trait::async_trait]
pub trait ProcessAuthorizationResponseUserCase {
async fn process_authorization_response(
&self,
cmd: command::ProcessAuthorizationResponseCommand,
) -> errors::ProcessAuthorizationServiceResult<()>;
}

View file

@ -0,0 +1,142 @@
use std::sync::Arc;
use url::Url;
use crate::auth::application::port::out::db::{
delete_oauth_state::DeleteOAuthState, oauth_state_exists::OAuthStateExists,
save_oauth_access_token::SaveOAuthAccessToken,
};
use crate::auth::application::port::out::forge::{
get_username::GetUsername, request_access_token::RequestAccessToken,
};
use super::{command, errors::*, ProcessAuthorizationResponseUserCase};
const STATE_LEN: usize = 8;
pub struct ProcessAuthorizationResponseProcessAuthorizationService {
oauth_state_exists_adapter: Arc<dyn OAuthStateExists>,
delete_oauth_state_adapter: Arc<dyn DeleteOAuthState>,
save_oauth_access_token_adapter: Arc<dyn SaveOAuthAccessToken>,
request_access_token_adapter: Arc<dyn RequestAccessToken>,
get_username_adapter: Arc<dyn GetUsername>,
process_authorization_response_redirect_uri: Url,
}
impl ProcessAuthorizationResponseProcessAuthorizationService {
pub fn new(
oauth_state_exists_adapter: Arc<dyn OAuthStateExists>,
delete_oauth_state_adapter: Arc<dyn DeleteOAuthState>,
save_oauth_access_token_adapter: Arc<dyn SaveOAuthAccessToken>,
request_access_token_adapter: Arc<dyn RequestAccessToken>,
get_username_adapter: Arc<dyn GetUsername>,
process_authorization_response_redirect_uri: Url,
) -> Self {
Self {
oauth_state_exists_adapter,
delete_oauth_state_adapter,
save_oauth_access_token_adapter,
request_access_token_adapter,
get_username_adapter,
process_authorization_response_redirect_uri,
}
}
}
#[async_trait::async_trait]
impl ProcessAuthorizationResponseUserCase
for ProcessAuthorizationResponseProcessAuthorizationService
{
async fn process_authorization_response(
&self,
cmd: command::ProcessAuthorizationResponseCommand,
) -> ProcessAuthorizationServiceResult<()> {
if let Some(u) = cmd.redirect_uri() {
if u.host() != self.process_authorization_response_redirect_uri.host()
&& u.path() != self.process_authorization_response_redirect_uri.path()
{
return Err(ProcessAuthorizationServiceError::BadRequest);
}
}
if !self
.oauth_state_exists_adapter
.oauth_state_exists(
cmd.state(),
cmd.oauth_provider(),
&self.process_authorization_response_redirect_uri,
)
.await?
{
return Err(ProcessAuthorizationServiceError::BadRequest);
}
self.delete_oauth_state_adapter
.delete_oauth_state(cmd.state(), cmd.oauth_provider())
.await?;
let access_token = self
.request_access_token_adapter
.request_access_token(
cmd.code().into(),
self.process_authorization_response_redirect_uri.clone(),
)
.await?;
let username = self
.get_username_adapter
.get_username(&access_token)
.await?;
self.save_oauth_access_token_adapter
.save_oauth_access_token(&username, &cmd.oauth_provider(), &access_token)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::application::port::out::db::{
delete_oauth_state::tests::*, oauth_state_exists::tests::*,
save_oauth_access_token::tests::*,
};
use crate::auth::application::port::out::forge::{
get_username::tests::*, request_access_token::tests::*,
};
use crate::tests::bdd::*;
#[actix_rt::test]
async fn test_process_authorization_response() {
let username = "foo";
let state = "bar";
let code = "baz";
let oauth_provider = "test_process_authorization_response_service";
let url = Url::parse(&format!("http://{oauth_provider}")).unwrap();
let mut redirect_uri = url.clone();
redirect_uri.set_query(Some(&format!("state={state}")));
let cmd = command::ProcessAuthorizationResponseCommand::new_command(
Some(redirect_uri.clone()),
state.into(),
code.into(),
oauth_provider.into(),
)
.unwrap();
let s = ProcessAuthorizationResponseProcessAuthorizationService::new(
mock_oauth_state_exists(IS_CALLED_ONLY_ONCE, RETURNS_TRUE),
mock_delete_oauth_state(IS_CALLED_ONLY_ONCE),
mock_save_oauth_access_token(IS_CALLED_ONLY_ONCE),
mock_request_access_token(IS_CALLED_ONLY_ONCE),
mock_get_username(username.into(), IS_CALLED_ONLY_ONCE),
redirect_uri,
);
s.process_authorization_response(cmd).await.unwrap();
}
}