#[macro_use] extern crate rocket; use chrono::{DateTime, Utc}; use rocket::fs::FileServer; use rocket::http::uri::Reference; use rocket::http::{Cookie, CookieJar, Status}; use rocket::outcome::Outcome; use rocket::request::{self, FromRequest}; use rocket::response::{Redirect, content}; use rocket::Request; use serde::{Deserialize, Serialize}; use std::result::Result; use rocket::serde::json::Json; use openidconnect::core::{ CoreAuthenticationFlow, CoreClient, CoreProviderMetadata, CoreResponseType, CoreUserInfoClaims, }; use openidconnect::reqwest::async_http_client; use openidconnect::url::Url; use openidconnect::{ AccessTokenHash, AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, }; const USER_COOKIE: &'static str = "user"; #[derive(Serialize, Deserialize)] struct UserCookie { access_token: openidconnect::AccessToken, expiration: DateTime, refresh_token: openidconnect::RefreshToken, } struct User {} impl User { async fn from_request_helper(req: &Request<'_>) -> Result { let cookie = req .cookies() .get_private(USER_COOKIE) .ok_or(anyhow::anyhow!("no cookie"))?; let user_cookie: UserCookie = serde_json::from_str(cookie.value())?; let client = keycloak_client().await; let token = client .exchange_refresh_token(&user_cookie.refresh_token) .request_async(async_http_client) .await?; let user_info: CoreUserInfoClaims = client .user_info(token.access_token().clone(), None)? .request_async(async_http_client) .await?; log::info!("Got user_info: {:?}", user_info); Ok(User {}) } } #[rocket::async_trait] impl<'r> FromRequest<'r> for User { type Error = anyhow::Error; async fn from_request(req: &'r Request<'_>) -> request::Outcome { match User::from_request_helper(req).await { Ok(user) => Outcome::Success(User {}), Err(error) => Outcome::Failure((Status::Forbidden, error)), } } } #[get("/")] fn index(user: Option) -> String { match user { None => "Not logged in".to_string(), Some(user) => "Logged in".to_string(), } } #[get("/test")] fn test() -> Json { Json(String::from("test")) } async fn keycloak_client() -> CoreClient { // // Use OpenID Connect Discovery to fetch the provider metadata. let provider_metadata = CoreProviderMetadata::discover_async( IssuerUrl::new("https://auth.orbekk.com/realms/test".to_string()).unwrap(), async_http_client, ) .await .unwrap(); let client = CoreClient::from_provider_metadata( provider_metadata, ClientId::new("test-client".to_string()), Some(ClientSecret::new( "EbIMIpGnYPrG1GBl6eZtVM5zIhiuu5p1".to_string(), )), ) // Set the URL the user will be redirected to after the authorization process. .set_redirect_uri( RedirectUrl::new("https://bridge.orbekk.com/keycloak-callback".to_string()).unwrap(), ); client } #[get("/login")] async fn login() -> Redirect { let (auth_url, csrf_token, nonce) = keycloak_client() .await .authorize_url( AuthenticationFlow::::AuthorizationCode, CsrfToken::new_random, Nonce::new_random, ) .add_scope(Scope::new("email".to_string())) .add_scope(Scope::new("profile".to_string())) .url(); log::info!("{:?}", auth_url); Redirect::to(Reference::parse_owned(auth_url.into()).unwrap()) } #[get("/keycloak-callback?&")] async fn keycloak_callback(jar: &CookieJar<'_>, code: &str, state: &str) -> Redirect { // TODO: Validate state let request_time = Utc::now(); let token = keycloak_client() .await .exchange_code(AuthorizationCode::new(code.to_string())) .request_async(async_http_client) .await .unwrap(); log::info!("token: {:?}", token); log::info!("access token {:?}", token.access_token().secret()); log::info!( "refresh token {:?}", token.refresh_token().unwrap().secret() ); let expiration = request_time + chrono::Duration::from_std(token.expires_in().unwrap()).unwrap(); jar.add_private(Cookie::new( USER_COOKIE, serde_json::to_string(&UserCookie { access_token: token.access_token().clone(), expiration, refresh_token: token.refresh_token().unwrap().clone(), }) .unwrap(), )); Redirect::to(uri!(index)) } #[rocket::main] async fn main() -> Result<(), anyhow::Error> { dotenv::dotenv().ok(); env_logger::init(); rocket::build() .mount("/api", routes![index, test, login, keycloak_callback]) .mount("/", FileServer::from(std::env::var("WEBAPP_PATH").unwrap())) .launch() .await?; Ok(()) }