diff options
author | Kjetil Orbekk <kj@orbekk.com> | 2022-10-07 16:59:29 -0400 |
---|---|---|
committer | Kjetil Orbekk <kj@orbekk.com> | 2022-10-07 16:59:29 -0400 |
commit | c64a7a640ac8c59eb6339f0a06d2ad2efab3fd11 (patch) | |
tree | 229ccf88da26d5339aadab98013834b6c2af6cbc /server/src | |
parent | 01753ebd32e4e0fa8adb11fb02a77720773e3018 (diff) |
Start working on authentication
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/auth.rs | 97 | ||||
-rw-r--r-- | server/src/main.rs | 79 |
2 files changed, 165 insertions, 11 deletions
diff --git a/server/src/auth.rs b/server/src/auth.rs new file mode 100644 index 0000000..ab1ba8b --- /dev/null +++ b/server/src/auth.rs @@ -0,0 +1,97 @@ +use std::{ + env, + num::NonZeroUsize, + sync::{Arc, Mutex}, +}; + +use lru::LruCache; +use openidconnect::{ + core::{CoreClient, CoreProviderMetadata, CoreResponseType}, + reqwest::async_http_client, + url::Url, + AccessTokenHash, AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, + IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, +}; +use uuid::Uuid; +use serde::{Deserialize, Serialize}; + +pub struct LoginState { + csrf_token: CsrfToken, + nonce: Nonce, +} + +pub struct Authenticator { + pub client: CoreClient, + pub login_cache: Arc<Mutex<LruCache<EndUserId, LoginState>>>, +} + +#[derive(Eq, PartialEq, Hash, Debug, Clone, Serialize, Deserialize)] +pub struct EndUserId(Uuid); + +impl EndUserId { + pub fn new() -> Self { + Self(Uuid::new_v4()) + } +} + +const LOGIN_CACHE_SIZE: usize = 50; + +pub const LOGIN_CALLBACK: &'static str = "/api/login_callback"; +fn redirect_url(app_url: &str) -> RedirectUrl { + RedirectUrl::new(format!("{}{}", app_url, LOGIN_CALLBACK)).unwrap() +} + +impl Authenticator { + pub async fn new( + issuer_url: IssuerUrl, + client_id: ClientId, + client_secret: ClientSecret, + redirect_uri: RedirectUrl, + ) -> Self { + // Use OpenID Connect Discovery to fetch the provider metadata. + let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client) + .await + .unwrap(); + + let client = + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) + // Set the URL the user will be redirected to after the authorization process. + .set_redirect_uri(redirect_uri); + + Self { + client, + login_cache: Arc::new(Mutex::new(LruCache::new( + NonZeroUsize::new(LOGIN_CACHE_SIZE).unwrap(), + ))), + } + } + + pub async fn from_env() -> Self { + let app_url = env::var("APP_URL").unwrap(); + Authenticator::new( + IssuerUrl::new(env::var("OPENID_ISSUER_URL").unwrap()).unwrap(), + ClientId::new(env::var("OPENID_CLIENT_ID").unwrap()), + ClientSecret::new(env::var("OPENID_CLIENT_SECRET").unwrap()), + redirect_url(&app_url), + ) + .await + } + + pub async fn get_login_url(&self) -> (EndUserId, Url) { + let (auth_url, csrf_token, nonce) = self + .client + .authorize_url( + AuthenticationFlow::<CoreResponseType>::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("profile".to_string())) + .url(); + let user_id = EndUserId::new(); + self.login_cache + .lock().unwrap() + .put(user_id.clone(), LoginState { csrf_token, nonce }); + (user_id, auth_url) + } +} diff --git a/server/src/main.rs b/server/src/main.rs index aaa5798..994c291 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,10 +1,46 @@ -use std::env; +use std::{env, sync::Arc}; -use axum::{routing::get, Json, Router}; -use data::MyMessage; -use tracing::info; +use axum::{extract::{FromRequest, Extension}, routing::get, Json, Router, http::{Request, request::Parts}, body::Body}; +use openidconnect::{ + core::{CoreClient, CoreProviderMetadata, CoreResponseType}, + reqwest::async_http_client, + AccessTokenHash, AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, + IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, url::Url, +}; +use protocol::UserInfo; +use tower_http::trace::TraceLayer; +use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; +use tracing::{info, trace}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use tower_http::{trace::TraceLayer}; +mod auth; +use crate::auth::Authenticator; + +struct ServerContext { + pub app_url: String, + pub authenticator: Authenticator, +} +type ContextExtension = Extension<Arc<ServerContext>>; + +async fn keycloak_client( + issuer_url: IssuerUrl, + client_id: ClientId, + client_secret: ClientSecret, + redirect_uri: RedirectUrl, +) -> CoreClient { + // // Use OpenID Connect Discovery to fetch the provider metadata. + let provider_metadata = CoreProviderMetadata::discover_async(issuer_url, async_http_client) + .await + .unwrap(); + + let client = + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) + // Set the URL the user will be redirected to after the authorization process. + .set_redirect_uri( + redirect_uri + ); + + client +} #[tokio::main] async fn main() { @@ -16,12 +52,23 @@ async fn main() { )) .with(tracing_subscriber::fmt::layer()) .init(); - + let bind_address = env::var("BIND_ADDRESS").unwrap(); info!("Starting server on {}", bind_address); + let app_url = env::var("APP_URL").unwrap(); + + let state = Arc::new(ServerContext { + app_url: app_url, + authenticator: Authenticator::from_env().await, + }); + let app = Router::new() - .route("/api/test", get(test)) + .route("/api/user/info", get(user_info)) + .route("/api/get_login_url", get(get_login_url)) + .route(auth::LOGIN_CALLBACK, get(login_callback)) + .layer(CookieManagerLayer::new()) + .layer(Extension(state)) .layer(TraceLayer::new_for_http()); axum::Server::bind(&bind_address.parse().unwrap()) @@ -30,8 +77,18 @@ async fn main() { .unwrap(); } -async fn test() -> Json<MyMessage> { - Json(MyMessage { - message: "Hello, ,World!".to_string(), - }) +async fn user_info() -> Json<Option<UserInfo>> { + Json(None) +} + +async fn login_callback(mut req: Parts) -> &'static str { + info!("{req:?}"); + "hello" +} + +async fn get_login_url(extension: ContextExtension) -> Json<Url> { + let (user_id, auth_url) = extension.authenticator.get_login_url().await; + trace!("Creating auth url for {user_id:?}"); + // cookies.add(Cookie::new("user-id", serde_json::to_string(user_id))); + Json(auth_url) } |