Oauth shit
This commit is contained in:
parent
38ad623983
commit
036e74b34e
3 changed files with 1337 additions and 47 deletions
1134
Cargo.lock
generated
1134
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,6 +5,10 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = { version = "0.7.5", features = ["multipart", "macros"] }
|
axum = { version = "0.7.5", features = ["multipart", "macros"] }
|
||||||
|
axum-extra = { version = "0.9.3", features = ["cookie-private"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
oauth2 = "5.0.0-alpha"
|
||||||
|
reqwest = { version = "0.12.5", features = ["json"] }
|
||||||
serde = { version = "1.0.203", features = ["derive"] }
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
serde_json = "1.0.118"
|
serde_json = "1.0.118"
|
||||||
tokio = { version = "1.38.0", features = ["full"] }
|
tokio = { version = "1.38.0", features = ["full"] }
|
||||||
|
|
|
||||||
246
src/main.rs
246
src/main.rs
|
|
@ -4,21 +4,100 @@
|
||||||
//! cargo run -p example-readme
|
//! cargo run -p example-readme
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
extract::{FromRef, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, Redirect},
|
||||||
routing::{get, post},
|
routing::get,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use axum_extra::extract::{
|
||||||
|
cookie::{Cookie, Expiration, Key, SameSite},
|
||||||
|
CookieJar, PrivateCookieJar,
|
||||||
|
};
|
||||||
|
use base64::Engine;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use serde_json::Value;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
use oauth2::{
|
||||||
|
basic::*, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointNotSet,
|
||||||
|
EndpointSet, IntrospectionUrl, RedirectUrl, TokenResponse, TokenUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! unwrap_env {
|
||||||
|
($name:literal) => {
|
||||||
|
std::env::var($name).expect(&format!("missing `{}` env var", $name))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type OClient = BasicClient<EndpointSet, EndpointNotSet, EndpointSet, EndpointNotSet, EndpointSet>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
http: reqwest::Client,
|
||||||
|
oauth: OClient,
|
||||||
|
key: Key,
|
||||||
|
users: Arc<RwLock<HashSet<String>>>,
|
||||||
|
code: Arc<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRef<AppState> for Key {
|
||||||
|
fn from_ref(state: &AppState) -> Self {
|
||||||
|
state.key.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserLoggedIn;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct User42 {
|
||||||
|
groups: Vec<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// initialize tracing
|
// initialize tracing
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
let oauth = BasicClient::new(ClientId::new(unwrap_env!("CLIENT_ID")))
|
||||||
|
.set_redirect_uri(RedirectUrl::new(format!("http://localhost:3000/auth/callback")).unwrap())
|
||||||
|
.set_introspection_url(
|
||||||
|
IntrospectionUrl::new("https://api.intra.42.fr/oauth/token/info".to_string()).unwrap(),
|
||||||
|
)
|
||||||
|
.set_client_secret(ClientSecret::new(unwrap_env!("CLIENT_SECRET")))
|
||||||
|
.set_auth_uri(
|
||||||
|
AuthUrl::new("https://api.intra.42.fr/oauth/authorize".to_string())
|
||||||
|
.expect("invalid authUrl"),
|
||||||
|
)
|
||||||
|
.set_token_uri(
|
||||||
|
TokenUrl::new("https://api.intra.42.fr/oauth/token".to_string())
|
||||||
|
.expect("invalid tokenUrl"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let http = reqwest::ClientBuilder::new()
|
||||||
|
// Following redirects opens the client up to SSRF vulnerabilities.
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.build()
|
||||||
|
.expect("Client should build");
|
||||||
|
|
||||||
|
let cookie_secret = unwrap_env!("COOKIE_SECRET");
|
||||||
|
dbg!(&cookie_secret);
|
||||||
|
let base64_value = base64::engine::general_purpose::URL_SAFE
|
||||||
|
.decode(cookie_secret)
|
||||||
|
.unwrap();
|
||||||
|
let key: Key = Key::from(&base64_value);
|
||||||
|
|
||||||
|
let code = oauth
|
||||||
|
.exchange_client_credentials()
|
||||||
|
.request_async(&http)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
// build our application with a route
|
// build our application with a route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
// `GET /` goes to `root`
|
// `GET /` goes to `root`
|
||||||
|
|
@ -27,7 +106,16 @@ async fn main() {
|
||||||
.route("/stop", get(stop))
|
.route("/stop", get(stop))
|
||||||
.route("/start", get(start))
|
.route("/start", get(start))
|
||||||
.route("/restart", get(restart))
|
.route("/restart", get(restart))
|
||||||
.route("/config", get(get_config).post(post_config));
|
.route("/config", get(get_config))
|
||||||
|
.route("/auth/callback", get(oauth2_callback))
|
||||||
|
.route("/auth/login", get(oauth2_login))
|
||||||
|
.with_state(AppState {
|
||||||
|
key,
|
||||||
|
http,
|
||||||
|
oauth,
|
||||||
|
users: Default::default(),
|
||||||
|
code: dbg!(code.access_token().secret().clone().into()),
|
||||||
|
});
|
||||||
|
|
||||||
// run our app with hyper
|
// run our app with hyper
|
||||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
|
||||||
|
|
@ -37,6 +125,114 @@ async fn main() {
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn oauth2_login(
|
||||||
|
State(AppState {
|
||||||
|
http, oauth, users, ..
|
||||||
|
}): State<AppState>,
|
||||||
|
) -> Redirect {
|
||||||
|
let (url, _) = oauth.authorize_url(|| CsrfToken::new_random()).url() else {
|
||||||
|
dbg!("got an error");
|
||||||
|
return Redirect::to("/error/1");
|
||||||
|
};
|
||||||
|
dbg!(&url);
|
||||||
|
Redirect::to(url.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oauth2_callback(
|
||||||
|
State(AppState {
|
||||||
|
http,
|
||||||
|
oauth,
|
||||||
|
users,
|
||||||
|
code: app_code,
|
||||||
|
..
|
||||||
|
}): State<AppState>,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
jar: PrivateCookieJar,
|
||||||
|
) -> (PrivateCookieJar, Redirect) {
|
||||||
|
let Some(code) = params.get("code") else {
|
||||||
|
dbg!(());
|
||||||
|
return (jar, Redirect::to("/"));
|
||||||
|
};
|
||||||
|
let Some(state) = params.get("state") else {
|
||||||
|
dbg!(());
|
||||||
|
return (jar, Redirect::to("/"));
|
||||||
|
};
|
||||||
|
dbg!(&code);
|
||||||
|
|
||||||
|
let mut form_data = HashMap::new();
|
||||||
|
form_data.insert("grant_type", "authorization_code".to_string());
|
||||||
|
form_data.insert("client_id", unwrap_env!("CLIENT_ID"));
|
||||||
|
form_data.insert("client_secret", unwrap_env!("CLIENT_SECRET"));
|
||||||
|
form_data.insert("code", code.to_string());
|
||||||
|
form_data.insert("redirect_uri", oauth.redirect_uri().unwrap().to_string());
|
||||||
|
form_data.insert("state", state.to_string());
|
||||||
|
dbg!(&form_data);
|
||||||
|
let token_res = match http
|
||||||
|
.post(oauth.token_uri().as_str())
|
||||||
|
.form(&form_data)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(o) => o.json::<Value>().await.unwrap(),
|
||||||
|
Err(e) => {
|
||||||
|
error!("{e}");
|
||||||
|
return (jar, Redirect::to("/auth/"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dbg!(&token_res);
|
||||||
|
|
||||||
|
let Ok(rep) = http
|
||||||
|
.get("https://api.intra.42.fr/v2/me")
|
||||||
|
.bearer_auth(token_res["access_token"].as_str().unwrap())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| println!("{e}"))
|
||||||
|
else {
|
||||||
|
return (jar, Redirect::to("/error/2"));
|
||||||
|
};
|
||||||
|
let Ok(json) = rep.json::<Value>().await else {
|
||||||
|
return (jar, Redirect::to("/error/3"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = json["id"].as_u64().unwrap();
|
||||||
|
dbg!(&id);
|
||||||
|
let Ok(rep) = http
|
||||||
|
.get(dbg!(format!(
|
||||||
|
"https://api.intra.42.fr/v2/users/{}/groups",
|
||||||
|
id
|
||||||
|
)))
|
||||||
|
.query(&[
|
||||||
|
("page[size]", 100), /*("filter[id]", id.as_u64().unwrap())*/
|
||||||
|
])
|
||||||
|
.bearer_auth(&app_code)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| println!("{e}"))
|
||||||
|
else {
|
||||||
|
return (jar, Redirect::to("/error/2"));
|
||||||
|
};
|
||||||
|
let Ok(json) = rep.json::<Value>().await else {
|
||||||
|
return (jar, Redirect::to("/error/3"));
|
||||||
|
};
|
||||||
|
let is_tut = json
|
||||||
|
.as_array()
|
||||||
|
.map(|s| s.iter().any(|s| s["id"] == 166))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !is_tut {
|
||||||
|
return (jar, Redirect::to("https://maix.me/"));
|
||||||
|
}
|
||||||
|
let mut cookie = Cookie::new("token", id.to_string());
|
||||||
|
cookie.set_same_site(SameSite::None);
|
||||||
|
cookie.set_expires(None);
|
||||||
|
cookie.set_secure(Some(false));
|
||||||
|
users.write().unwrap().insert(id.to_string());
|
||||||
|
|
||||||
|
let jar = jar.add(cookie);
|
||||||
|
|
||||||
|
(jar, Redirect::to("/"))
|
||||||
|
}
|
||||||
|
|
||||||
// basic handler that responds with a static string
|
// basic handler that responds with a static string
|
||||||
async fn root() -> Html<&'static str> {
|
async fn root() -> Html<&'static str> {
|
||||||
info!("Request link page");
|
info!("Request link page");
|
||||||
|
|
@ -145,48 +341,6 @@ async fn get_config() -> Result<Json<BotConfig>, (StatusCode, String)> {
|
||||||
Ok(Json(val))
|
Ok(Json(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_config(Json(body): Json<BotConfig>) -> (StatusCode, String) {
|
|
||||||
info!("Posted config");
|
|
||||||
let Ok(mut file) = tokio::fs::File::open(
|
|
||||||
match std::env::var("CONFIG_PATH").map_err(|e| {
|
|
||||||
error!("Unset env {e}");
|
|
||||||
(
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
"please set env CONFIG_PATH".to_string(),
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => return e,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("Failed to open config file: {e}");
|
|
||||||
e
|
|
||||||
}) else {
|
|
||||||
return (
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!(
|
|
||||||
"failed to open config file: {}",
|
|
||||||
std::env::var("CONFIG_PATH").unwrap()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
match serde_json::to_string_pretty(&body) {
|
|
||||||
Err(e) => {
|
|
||||||
error!("Failed to convert to json {e}");
|
|
||||||
return (StatusCode::OK, "Done".to_string());
|
|
||||||
}
|
|
||||||
Ok(s) => match file.write(s.as_bytes()).await {
|
|
||||||
Err(e) => error!("Got an error when writing file: {e}"),
|
|
||||||
Ok(_) => (),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
(StatusCode::OK, "Done".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn status() -> Result<String, StatusCode> {
|
async fn status() -> Result<String, StatusCode> {
|
||||||
info!("Requested status");
|
info!("Requested status");
|
||||||
let mut output = tokio::process::Command::new("systemctl")
|
let mut output = tokio::process::Command::new("systemctl")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue