feat: OAuth authz callback service to fetch && save OAuth access token
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
parent
8fb056a669
commit
36fd8a3f88
17
src/auth/application/services/mod.rs
Normal file
17
src/auth/application/services/mod.rs
Normal 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
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<()>;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue