feat(auth): add authentication integration
Some checks failed
continuous-integration/drone/push Build is failing

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2023-08-20 14:08:40 +02:00
parent 48d09c8ae3
commit e6084a7f4e
13 changed files with 409 additions and 233 deletions

View File

@@ -1,7 +1,7 @@
use std::borrow::Cow;
use std::fmt::Display;
use crate::router::AppState;
use crate::zitadel::{IntrospectionConfig, IntrospectionState};
use async_sqlx_session::PostgresSessionStore;
use axum::extract::{FromRef, FromRequestParts, Query, State};
@@ -10,9 +10,9 @@ use axum::headers::{Authorization, Cookie};
use axum::http::request::Parts;
use axum::http::StatusCode;
use axum::http::{header::SET_COOKIE, HeaderMap};
use axum::response::{IntoResponse, Redirect};
use axum::response::{ErrorResponse, IntoResponse, Redirect};
use axum::routing::get;
use axum::{async_trait, RequestPartsExt, Router, TypedHeader};
use axum::{async_trait, Json, RequestPartsExt, Router, TypedHeader};
use axum_sessions::async_session::{Session, SessionStore};
use como_domain::users::User;
use como_infrastructure::register::ServiceRegister;
@@ -20,6 +20,7 @@ use oauth2::basic::BasicClient;
use oauth2::{reqwest::async_http_client, AuthorizationCode, CsrfToken, Scope, TokenResponse};
use oauth2::{RedirectUrl, TokenIntrospectionResponse};
use serde::Deserialize;
use serde_json::json;
use zitadel::oidc::introspection::introspect;
#[derive(Debug, Deserialize)]
@@ -27,17 +28,39 @@ pub struct ZitadelAuthParams {
return_url: Option<String>,
}
pub async fn zitadel_auth(State(client): State<BasicClient>) -> impl IntoResponse {
let (auth_url, _csrf_token) = client
.authorize_url(CsrfToken::new_random)
.add_scope(Scope::new("identify".to_string()))
.add_scope(Scope::new("openid".to_string()))
.url();
Redirect::to(auth_url.as_ref())
trait AnyhowExtensions<T, E>
where
E: Display,
{
fn into_response(self) -> Result<T, ErrorResponse>;
}
impl<T, E> AnyhowExtensions<T, E> for anyhow::Result<T, E>
where
E: Display,
{
fn into_response(self) -> Result<T, ErrorResponse> {
match self {
Ok(o) => Ok(o),
Err(e) => {
tracing::error!("failed with anyhow error: {}", e);
Err(ErrorResponse::from((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"status": "something",
})),
)))
}
}
}
}
pub static COOKIE_NAME: &str = "SESSION";
pub async fn zitadel_auth(
State(services): State<ServiceRegister>,
) -> Result<impl IntoResponse, ErrorResponse> {
let url = services.auth_service.login().await.into_response()?;
Ok(Redirect::to(&url.to_string()))
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
@@ -48,44 +71,15 @@ pub struct AuthRequest {
pub async fn login_authorized(
Query(query): Query<AuthRequest>,
State(store): State<PostgresSessionStore>,
State(introspection_state): State<IntrospectionState>,
) -> impl IntoResponse {
let token = oauth_client
.exchange_code(AuthorizationCode::new(query.code.clone()))
.request_async(async_http_client)
State(services): State<ServiceRegister>,
) -> Result<impl IntoResponse, ErrorResponse> {
let (headers, url) = services
.auth_service
.login_authorized(&query.code, &query.state)
.await
.unwrap();
.into_response()?;
let config = IntrospectionConfig::from_ref(&introspection_state);
let res = introspect(
&config.introspection_uri,
&config.authority,
&config.authentication,
token.access_token().secret(),
)
.await
.unwrap();
let mut session = Session::new();
session
.insert(
"user",
User {
id: res.sub().unwrap().into(),
},
)
.unwrap();
let cookie = store.store_session(session).await.unwrap().unwrap();
let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie);
let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, cookie.parse().unwrap());
(headers, Redirect::to("http://localhost:3000/dash/home"))
Ok((headers, Redirect::to(url.as_str())))
}
pub struct AuthController;
@@ -106,79 +100,61 @@ pub struct UserFromSession {
pub user: User,
}
pub static COOKIE_NAME: &str = "SESSION";
#[async_trait]
impl<S> FromRequestParts<S> for UserFromSession
where
PostgresSessionStore: FromRef<S>,
IntrospectionState: FromRef<S>,
ServiceRegister: FromRef<S>,
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let store = PostgresSessionStore::from_ref(state);
let services = ServiceRegister::from_ref(state);
let cookie: Option<TypedHeader<Cookie>> = parts.extract().await.unwrap();
let session_cookie = cookie.as_ref().and_then(|cookie| cookie.get(COOKIE_NAME));
if let None = session_cookie {
let introspection_state = IntrospectionState::from_ref(state);
// let introspection_state = IntrospectionState::from_ref(state);
let basic: Option<TypedHeader<Authorization<Basic>>> = parts.extract().await.unwrap();
// let basic: Option<TypedHeader<Authorization<Basic>>> = parts.extract().await.unwrap();
if let Some(basic) = basic {
let config = IntrospectionConfig::from_ref(&introspection_state);
// if let Some(basic) = basic {
// let config = IntrospectionConfig::from_ref(&introspection_state);
let res = introspect(
&config.introspection_uri,
&config.authority,
&config.authentication,
basic.password(),
)
.await
.unwrap();
// let res = introspect(
// &config.introspection_uri,
// &config.authority,
// &config.authentication,
// basic.password(),
// )
// .await
// .unwrap();
return Ok(UserFromSession {
user: User {
id: res.sub().unwrap().into(),
},
});
}
// return Ok(UserFromSession {
// user: User {
// id: res.sub().unwrap().into(),
// },
// });
// }
todo!()
return Err((StatusCode::UNAUTHORIZED, "No session was found"));
//return Err(anyhow::anyhow!("No session was found")).into_response();
}
let session_cookie = session_cookie.unwrap();
tracing::debug!(
"UserFromSession: got session cookie from user agent, {}={}",
COOKIE_NAME,
session_cookie
);
// continue to decode the session cookie
let user =
if let Some(session) = store.load_session(session_cookie.to_owned()).await.unwrap() {
if let Some(user) = session.get::<User>("user") {
tracing::debug!(
"UserFromSession: session decoded success, user_id={:?}",
user.id
);
user
} else {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"No `user_id` found in session",
));
}
} else {
tracing::debug!(
"UserIdFromSession: err session not exists in store, {}={}",
COOKIE_NAME,
session_cookie
);
return Err((StatusCode::BAD_REQUEST, "No session found for cookie"));
};
let user = services
.auth_service
.get_user_from_session(session_cookie)
.await
.into_response()
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "failed with error"))?;
Ok(UserFromSession { user })
Ok(UserFromSession {
user: User { id: user.id },
})
}
}

View File

@@ -1,18 +1,15 @@
use std::env;
use anyhow::Context;
use async_sqlx_session::PostgresSessionStore;
use axum::extract::FromRef;
use axum::http::{HeaderValue, Method};
use axum::Router;
use como_infrastructure::register::ServiceRegister;
use oauth2::basic::BasicClient;
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use crate::controllers::auth::AuthController;
use crate::controllers::graphql::GraphQLController;
use crate::zitadel::{IntrospectionState, IntrospectionStateBuilder};
pub struct Api;
@@ -22,18 +19,8 @@ impl Api {
cors_origin: &str,
service_register: ServiceRegister,
) -> anyhow::Result<()> {
let client_id = env::var("CLIENT_ID").expect("Missing CLIENT_ID!");
let client_secret = env::var("CLIENT_SECRET").expect("Missing CLIENT_SECRET!");
let zitadel_url = env::var("ZITADEL_URL").expect("missing ZITADEL_URL");
let is = IntrospectionStateBuilder::new(&zitadel_url)
.with_basic_auth(&client_id, &client_secret)
.build()
.await?;
let app_state = AppState {
store: service_register.session_store.clone(),
introspection_state: is,
service_register: service_register.clone(),
};
let router = Router::new()
@@ -76,18 +63,11 @@ impl Api {
#[derive(Clone)]
pub struct AppState {
introspection_state: IntrospectionState,
store: PostgresSessionStore,
service_register: ServiceRegister,
}
impl FromRef<AppState> for PostgresSessionStore {
fn from_ref(state: &AppState) -> Self {
state.store.clone()
}
}
impl FromRef<AppState> for IntrospectionState {
impl FromRef<AppState> for ServiceRegister {
fn from_ref(input: &AppState) -> Self {
input.introspection_state.clone()
input.service_register.clone()
}
}

View File

@@ -1,90 +1 @@
pub mod client;
use axum::extract::FromRef;
use openidconnect::IntrospectionUrl;
use zitadel::{
axum::introspection::IntrospectionStateBuilderError,
credentials::Application,
oidc::{discovery::discover, introspection::AuthorityAuthentication},
};
#[derive(Clone, Debug)]
pub struct IntrospectionState {
pub(crate) config: IntrospectionConfig,
}
/// Configuration that must be inject into the axum application state. Used by the
/// [IntrospectionStateBuilder](super::IntrospectionStateBuilder). This struct is also used to create the [IntrospectionState](IntrospectionState)
#[derive(Debug, Clone)]
pub struct IntrospectionConfig {
pub(crate) authority: String,
pub(crate) authentication: AuthorityAuthentication,
pub(crate) introspection_uri: IntrospectionUrl,
}
impl FromRef<IntrospectionState> for IntrospectionConfig {
fn from_ref(input: &IntrospectionState) -> Self {
input.config.clone()
}
}
pub struct IntrospectionStateBuilder {
authority: String,
authentication: Option<AuthorityAuthentication>,
}
/// Builder for [IntrospectionConfig]
impl IntrospectionStateBuilder {
pub fn new(authority: &str) -> Self {
Self {
authority: authority.to_string(),
authentication: None,
}
}
pub fn with_basic_auth(
&mut self,
client_id: &str,
client_secret: &str,
) -> &mut IntrospectionStateBuilder {
self.authentication = Some(AuthorityAuthentication::Basic {
client_id: client_id.to_string(),
client_secret: client_secret.to_string(),
});
self
}
pub fn with_jwt_profile(&mut self, application: Application) -> &mut IntrospectionStateBuilder {
self.authentication = Some(AuthorityAuthentication::JWTProfile { application });
self
}
pub async fn build(&mut self) -> Result<IntrospectionState, IntrospectionStateBuilderError> {
if self.authentication.is_none() {
return Err(IntrospectionStateBuilderError::NoAuthSchema);
}
let metadata = discover(&self.authority)
.await
.map_err(|source| IntrospectionStateBuilderError::Discovery { source })?;
let introspection_uri = metadata
.additional_metadata()
.introspection_endpoint
.clone();
if introspection_uri.is_none() {
return Err(IntrospectionStateBuilderError::NoIntrospectionUrl);
}
Ok(IntrospectionState {
config: IntrospectionConfig {
authority: self.authority.clone(),
introspection_uri: introspection_uri.unwrap(),
authentication: self.authentication.as_ref().unwrap().clone(),
},
})
}
}