Last active
October 31, 2022 18:48
-
-
Save Akanoa/94153a3e50fd8a5876e8dd4caaa25ceb to your computer and use it in GitHub Desktop.
Gitlab SSO
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "gitlab-sso" | |
version = "0.1.0" | |
edition = "2021" | |
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |
[dependencies] | |
actix-rt = "2.7.0" | |
actix-web = "4.2.1" | |
actix-session = {version = "0.7.2", features = ["cookie-session"]} | |
actix-files = "0.6.2" | |
serde = {version = "1.0.145", features = ["derive"]} | |
serde_json = "1.0.86" | |
oauth2 = "4.2.3" | |
eyre = "0.6.8" | |
env_logger = "0.9.0" | |
log = "0.4.17" | |
nanoid = "0.4.0" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use crate::header::HeaderMap; | |
use actix_files as fs; | |
use actix_files::NamedFile; | |
use actix_session::{storage::CookieSessionStore, Session, SessionExt, SessionMiddleware}; | |
use actix_web::cookie::Key; | |
use actix_web::error::InternalError; | |
use actix_web::guard::GuardContext; | |
use actix_web::web::{Data, Json}; | |
use actix_web::{delete, get, post, web, App, HttpResponse, HttpServer, Responder, Result}; | |
use eyre::WrapErr; | |
use log::debug; | |
use nanoid::nanoid; | |
use oauth2::basic::BasicClient; | |
use oauth2::http::{header, Method}; | |
use oauth2::reqwest::http_client; | |
use oauth2::url::Url; | |
use oauth2::{ | |
AccessToken, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, PkceCodeChallenge, | |
RedirectUrl, Scope, TokenResponse, TokenUrl, | |
}; | |
use serde::{Deserialize, Serialize}; | |
use std::collections::HashMap; | |
use std::env; | |
use std::sync::Mutex; | |
#[derive(Serialize)] | |
struct Identitity { | |
name: String, | |
tokens: Vec<Token>, | |
} | |
#[derive(Serialize, Clone)] | |
struct Token { | |
description: String, | |
tenant: String, | |
token: String, | |
} | |
#[derive(Deserialize)] | |
struct CreateData { | |
description: String, | |
} | |
#[derive(Deserialize)] | |
struct DeleteData { | |
tenant: String, | |
} | |
struct AppState { | |
oauth: BasicClient, | |
api_base_url: String, | |
tokens: HashMap<String, Token>, | |
} | |
#[derive(Deserialize)] | |
struct AuthRequest { | |
code: String, | |
state: String, | |
} | |
#[derive(Deserialize, Debug)] | |
pub struct UserData { | |
id: u64, | |
name: String, | |
username: String, | |
email: String, | |
} | |
fn api_guard(ctx: &GuardContext) -> bool { | |
if let Ok(Some(_)) = ctx.get_session().get::<String>("login") { | |
true | |
} else { | |
false | |
} | |
} | |
#[get("/")] | |
async fn index(session: Session) -> Result<fs::NamedFile> { | |
let login_value = session | |
.get::<String>("login") | |
.map_err(actix_web::error::ErrorBadRequest)?; | |
if login_value.is_some() { | |
Ok(NamedFile::open("assets/static/index.html")?) | |
} else { | |
Ok(NamedFile::open("assets/static/login.html")?) | |
} | |
} | |
#[get("/logout")] | |
async fn logout(session: Session) -> HttpResponse { | |
session.remove("login"); | |
HttpResponse::Found() | |
.append_header((header::LOCATION, "/".to_string())) | |
.finish() | |
} | |
#[get("/api/list", guard = "api_guard")] | |
async fn list(app: Data<Mutex<AppState>>) -> Result<impl Responder> { | |
if let Ok(guard) = app.lock() { | |
let tokens = guard | |
.tokens | |
.values() | |
.into_iter() | |
.map(|c| c.clone()) | |
.collect::<Vec<Token>>(); | |
let identity = Identitity { | |
name: "toto".to_string(), | |
tokens, | |
}; | |
Ok(Json(identity)) | |
} else { | |
Err(actix_web::error::ErrorInternalServerError( | |
"Unable to get identity", | |
)) | |
} | |
} | |
#[delete("/api/delete", guard = "api_guard")] | |
async fn delete(form: web::Json<DeleteData>, app: Data<Mutex<AppState>>) -> Result<impl Responder> { | |
if let Ok(mut guard) = app.lock() { | |
guard.tokens.remove(&form.tenant); | |
let tokens = guard | |
.tokens | |
.values() | |
.into_iter() | |
.map(|c| c.clone()) | |
.collect::<Vec<Token>>(); | |
let identity = Identitity { | |
name: "toto".to_string(), | |
tokens, | |
}; | |
Ok(Json(identity)) | |
} else { | |
Err(actix_web::error::ErrorInternalServerError( | |
"Unable to get identity", | |
)) | |
} | |
} | |
#[post("/api/create", guard = "api_guard")] | |
async fn create(form: web::Json<CreateData>, app: Data<Mutex<AppState>>) -> Result<impl Responder> { | |
let token = Token { | |
description: form.description.to_string(), | |
tenant: nanoid!(5, &nanoid::alphabet::SAFE), | |
token: nanoid!(128, &nanoid::alphabet::SAFE), | |
}; | |
if let Ok(mut guard) = app.lock() { | |
guard.tokens.insert(token.tenant.to_string(), token); | |
let tokens = guard | |
.tokens | |
.values() | |
.into_iter() | |
.map(|c| c.clone()) | |
.collect::<Vec<Token>>(); | |
let identity = Identitity { | |
name: "toto".to_string(), | |
tokens, | |
}; | |
Ok(Json(identity)) | |
} else { | |
Err(actix_web::error::ErrorInternalServerError( | |
"Unable to create token", | |
)) | |
} | |
} | |
#[get("/login")] | |
async fn login(session: Session, data: web::Data<Mutex<AppState>>) -> HttpResponse { | |
// Generate a PKCE challenge | |
// https://oa.dnc.global/-fr-.html?page=unarticle&id_article=148 | |
let (pkce_challenge, pcke_verifier) = PkceCodeChallenge::new_random_sha256(); | |
let a = session | |
.insert("pkce_verifier", pcke_verifier) | |
.wrap_err("Unable to save pkce verifier"); | |
if let Err(err) = a { | |
debug!("{:?}", err) | |
} | |
if let Ok(data) = data.lock() { | |
let (auth_url, _) = &data | |
.oauth | |
.authorize_url(CsrfToken::new_random) | |
.add_scope(Scope::new("profile".to_string())) | |
.add_scope(Scope::new("read_user".to_string())) | |
.set_pkce_challenge(pkce_challenge) | |
.url(); | |
HttpResponse::Found() | |
.append_header((header::LOCATION, auth_url.to_string())) | |
.finish() | |
} else { | |
HttpResponse::BadRequest().finish() | |
} | |
} | |
fn read_user(api_base_url: &str, access_token: &AccessToken) -> eyre::Result<UserData> { | |
let url = Url::parse( | |
format!( | |
"{}/user?access_token={}", | |
api_base_url, | |
access_token.secret() | |
) | |
.as_str(), | |
) | |
.wrap_err("Unable to parse URL")?; | |
let headers = HeaderMap::new(); | |
let response = http_client(oauth2::HttpRequest { | |
url, | |
method: Method::GET, | |
headers, | |
body: vec![], | |
}) | |
.wrap_err("Unable to Get user details")?; | |
serde_json::from_slice(&response.body).wrap_err("Unable to deserialize") | |
} | |
#[get("/auth")] | |
async fn auth( | |
session: Session, | |
data: web::Data<Mutex<AppState>>, | |
params: web::Query<AuthRequest>, | |
) -> HttpResponse { | |
let code = AuthorizationCode::new(params.code.clone()); | |
let _state = CsrfToken::new(params.state.clone()); | |
let pkce_verifier = session | |
.get("pkce_verifier") | |
.expect("Unable to get pkce verifier from session"); | |
if let Ok(data) = data.lock() { | |
match pkce_verifier { | |
Some(pkce_verifier) => { | |
let token = &data | |
.oauth | |
.exchange_code(code) | |
.set_pkce_verifier(pkce_verifier) | |
.request(http_client); | |
match token { | |
Ok(token) => { | |
let user_info = read_user(&data.api_base_url, token.access_token()); | |
match user_info { | |
Ok(user_info) => { | |
if let Err(_) = session.insert("login", user_info.email.clone()) { | |
return HttpResponse::InternalServerError().finish(); | |
} | |
HttpResponse::Found() | |
.append_header(("Location", "/")) | |
.finish() | |
} | |
Err(err) => { | |
log::error!("{:?}", err); | |
log::warn!("Unable to get user data"); | |
HttpResponse::BadRequest().finish() | |
} | |
} | |
} | |
Err(err) => { | |
log::error!("{:?}", err); | |
log::warn!("Unable to get user data"); | |
HttpResponse::BadRequest().finish() | |
} | |
} | |
} | |
None => { | |
dbg!("Unable to found pkce verifier"); | |
HttpResponse::BadRequest().finish() | |
} | |
} | |
} else { | |
HttpResponse::BadRequest().finish() | |
} | |
} | |
fn get_secret() -> Key { | |
let secret = env::var("APP_SECRET").expect("Missing APP_SECRET environment variable"); | |
Key::from(secret.as_bytes()) | |
} | |
#[actix_rt::main] | |
async fn main() { | |
std::env::set_var("RUST_LOG", "debug"); | |
env_logger::init(); | |
let app_host = env::var("APP_HOST").expect("Missing APP_HOST environment variable."); | |
let app_port = env::var("APP_PORT").expect("Missing APP_PORT environment variable."); | |
HttpServer::new(|| { | |
let application_id = ClientId::new( | |
env::var("GITLAB_SSO_APP_ID").expect("Missing GITLAB_SSO_APP_ID environment variable."), | |
); | |
let application_secret = ClientSecret::new( | |
env::var("GITLAB_SSO_APP_SECRET") | |
.expect("Missing GITLAB_SSO_APP_SECRET environment variable."), | |
); | |
let oauth_server = | |
env::var("GITLAB_SERVER").expect("Missing GITLAB_SERVER environment variable."); | |
let auth_url = AuthUrl::new(format!("https://{}/oauth/authorize", oauth_server)) | |
.expect("Invalid authorization endpoint URL"); | |
let token_url = TokenUrl::new(format!("https://{}/oauth/token", oauth_server)) | |
.expect("Invalid token endpoint URL"); | |
let redirect_url = env::var("GITLAB_SSO_REDIRECT_URL") | |
.expect("Missing GITLAB_SSO_REDIRECT_URL environment variable."); | |
let api_base_url = format!("https://{}/api/v4", oauth_server); | |
let client = BasicClient::new( | |
application_id, | |
Some(application_secret), | |
auth_url, | |
Some(token_url), | |
) | |
.set_redirect_uri(RedirectUrl::new(redirect_url).expect("Invalid redirect URL")); | |
println!("Running"); | |
App::new() | |
.app_data(Data::new(Mutex::new(AppState { | |
oauth: client, | |
api_base_url, | |
tokens: HashMap::new(), | |
}))) | |
.wrap(SessionMiddleware::new( | |
CookieSessionStore::default(), | |
get_secret(), | |
)) | |
.service( | |
fs::Files::new("/static", "./assets/static") | |
.show_files_listing() | |
.index_file("login.html"), | |
) | |
.service(index) | |
.service(login) | |
.service(logout) | |
.service(auth) | |
.service(create) | |
.service(list) | |
.service(delete) | |
}) | |
.bind(format!("{}:{}", app_host, app_port)) | |
.expect(format!("Can not bind to port {}", app_port).as_str()) | |
.run() | |
.await | |
.expect("Unable to run"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment