init
This commit is contained in:
commit
3896c5a93c
7 changed files with 2674 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
/data
|
||||||
|
/.env
|
2114
Cargo.lock
generated
Normal file
2114
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "bin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.27.0", features = ["full"] }
|
||||||
|
tokio-util = { version="0.7", features=["io"]}
|
||||||
|
futures-util = "0.3"
|
||||||
|
axum = {version="0.6", features=["macros", "headers"]}
|
||||||
|
serde = "1.0"
|
||||||
|
serde_cbor = "0.11"
|
||||||
|
openidconnect = "3.0"
|
||||||
|
render = { git="https://github.com/render-rs/render.rs" }
|
||||||
|
thiserror = "1.0.40"
|
||||||
|
rand = "0.8.5"
|
||||||
|
dotenvy = "0.15"
|
||||||
|
reqwest = { version="0.11", default_features=false}
|
||||||
|
markdown = "0.3.0"
|
18
src/item_explanation.md
Normal file
18
src/item_explanation.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# zettoIT bin
|
||||||
|
An empty bin was created for you. The first HTTP POST request can upload data.
|
||||||
|
All following requests can only read the uploaded data.
|
||||||
|
|
||||||
|
To use the build-in link-shortener functionality you have to POST the URL with Content-Type: text/x-uri or Content-Type: text/uri-list.
|
||||||
|
|
||||||
|
## Upload a link
|
||||||
|
`$ curl -H "Content-Type: text/x-uri" --data "https://example.com" <bin_url>`
|
||||||
|
|
||||||
|
## Upload a image
|
||||||
|
`$ curl -H "Content-Type: image/png" --data-binary @my-image.png <bin_url>`
|
||||||
|
|
||||||
|
## Upload a big file
|
||||||
|
`$ curl -G "Content-Type: application/gzip" -T my-file.tar.gz <bin_url>`
|
||||||
|
|
||||||
|
## Accessing the data
|
||||||
|
After uploading data you can access it by accessing <bin_url> with an optional file extension that suits the data that you uploaded.
|
||||||
|
If the bin is a link you will get redirected.
|
217
src/main.rs
Normal file
217
src/main.rs
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
use std::{collections::HashMap, env, sync::Arc};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
body::StreamBody,
|
||||||
|
extract::{BodyStream, Path, State},
|
||||||
|
headers::ContentType,
|
||||||
|
http::{header, HeaderMap, StatusCode},
|
||||||
|
response::{Html, IntoResponse, Redirect},
|
||||||
|
routing::get,
|
||||||
|
Router, TypedHeader,
|
||||||
|
};
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use metadata::Metadata;
|
||||||
|
use openid::Login;
|
||||||
|
use render::{html, raw};
|
||||||
|
use tokio::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod openid;
|
||||||
|
|
||||||
|
// RFC 7230 section 3.1.1
|
||||||
|
// It is RECOMMENDED that all HTTP senders and recipients
|
||||||
|
// support, at a minimum, request-line lengths of 8000 octets.
|
||||||
|
const HTTP_URL_MAXLENGTH: u64 = 8000;
|
||||||
|
|
||||||
|
// support the RFC2483 with text/uri-list and the inofficial text/x-uri mimetype
|
||||||
|
const HTTP_URL_MIMETYPES: [&str; 2] = ["text/x-uri", "text/uri-list"];
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum Error {
|
||||||
|
#[error("item could not be found")]
|
||||||
|
ItemNotFound,
|
||||||
|
#[error("errorfile exists")]
|
||||||
|
DataFileExists,
|
||||||
|
#[error("datafile without metafile")]
|
||||||
|
DataFileWithoutMetaFile,
|
||||||
|
}
|
||||||
|
type HandlerResult<T> = Result<T, Error>;
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
println!("main error: {:?}", self);
|
||||||
|
match self {
|
||||||
|
Self::ItemNotFound => (StatusCode::NOT_FOUND, "item could not be found"),
|
||||||
|
Self::DataFileExists => (StatusCode::CONFLICT, "item already has data"),
|
||||||
|
Self::DataFileWithoutMetaFile => {
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
path: String,
|
||||||
|
application_base: String,
|
||||||
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
logins: Arc<Mutex<HashMap<String, Login>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
let application_base = env::var("APPLICATION_BASE").expect("APPLICATION_BASE env var");
|
||||||
|
let issuer = env::var("ISSUER").expect("ISSUER env var");
|
||||||
|
let client_id = env::var("CLIENT_ID").expect("CLIENT_ID env var");
|
||||||
|
let client_secret = env::var("CLIENT_SECRET").ok();
|
||||||
|
let scopes = env::var("SCOPES")
|
||||||
|
.expect("SCOPES env var")
|
||||||
|
.split(' ')
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.to_owned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let state: AppState = AppState {
|
||||||
|
path: "data".to_string(),
|
||||||
|
application_base,
|
||||||
|
issuer,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
scopes,
|
||||||
|
logins: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/", get(openid::handle_login))
|
||||||
|
.route("/login/:id", get(openid::handle_callback))
|
||||||
|
.route("/:id", get(get_item).post(post_item))
|
||||||
|
.with_state(state);
|
||||||
|
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_item(
|
||||||
|
Path(id): Path<String>,
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
TypedHeader(content_type): TypedHeader<ContentType>,
|
||||||
|
mut stream: BodyStream,
|
||||||
|
) -> HandlerResult<impl IntoResponse> {
|
||||||
|
let id = sanitize_id(id);
|
||||||
|
|
||||||
|
let mut metadata = Metadata::from_file(&app_state.path, &id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::ItemNotFound)?;
|
||||||
|
|
||||||
|
if fs::metadata(&format!("{}/{}.data", app_state.path, &id))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
let mut data_file = File::create(&format!("{}/{}.data", app_state.path, &id))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let buf = chunk.map(|x| x.to_vec()).unwrap_or_default();
|
||||||
|
data_file.write_all(&buf).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.mimetype = Some(content_type.to_string());
|
||||||
|
|
||||||
|
metadata.to_file(&app_state.path, &id).await.unwrap();
|
||||||
|
|
||||||
|
Ok((StatusCode::CREATED, "OK"))
|
||||||
|
} else {
|
||||||
|
Err(Error::DataFileExists)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn get_item(
|
||||||
|
Path(id): Path<String>,
|
||||||
|
State(app_state): State<AppState>,
|
||||||
|
) -> HandlerResult<impl IntoResponse> {
|
||||||
|
let id = sanitize_id(id);
|
||||||
|
|
||||||
|
let metadata = Metadata::from_file(&app_state.path, &id).await.ok();
|
||||||
|
|
||||||
|
let data_file = File::open(&format!("{}/{}.data", app_state.path, &id))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
match (metadata, data_file) {
|
||||||
|
(None, None) => Err(Error::ItemNotFound),
|
||||||
|
(Some(_), None) => {
|
||||||
|
let body = include_str!("item_explanation.md").replace(
|
||||||
|
"<bin_url>",
|
||||||
|
&format!("{}{}", app_state.application_base, id),
|
||||||
|
);
|
||||||
|
let body = markdown::to_html(&body);
|
||||||
|
let body = html! {
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{"zettoit bin"}</title>
|
||||||
|
<link rel={"icon"} type={"image/svg"} href={"https://static.zettoit.eu/img/zettoit-logo.svg"}/>
|
||||||
|
</head>
|
||||||
|
<body style={"font-family: monospace; background-color: black; color: white;"}>
|
||||||
|
<div style={"margin: auto; max-width: 80ch;"}>
|
||||||
|
{raw!(body.as_str())}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
};
|
||||||
|
Ok((StatusCode::ACCEPTED, Html(body)).into_response())
|
||||||
|
}
|
||||||
|
(Some(metadata), Some(mut data_file)) => {
|
||||||
|
let data_file_metadata = fs::metadata(&format!("{}/{}.data", app_state.path, &id))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
if HTTP_URL_MIMETYPES.contains(&metadata.mimetype.as_deref().unwrap_or(""))
|
||||||
|
&& data_file_metadata.len() <= HTTP_URL_MAXLENGTH
|
||||||
|
{
|
||||||
|
let mut url = String::new();
|
||||||
|
data_file.read_to_string(&mut url).await.unwrap();
|
||||||
|
|
||||||
|
// Use the first line that doesn't start with a # to be compliant with RFC2483.
|
||||||
|
let url = url
|
||||||
|
.lines()
|
||||||
|
.into_iter()
|
||||||
|
.find(|x| !x.starts_with('#'))
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
Ok(Redirect::temporary(url).into_response())
|
||||||
|
} else {
|
||||||
|
let reader = ReaderStream::new(data_file);
|
||||||
|
let body = StreamBody::new(reader);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
metadata.mimetype.as_deref().unwrap_or("").parse().unwrap(),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
header::CONTENT_LENGTH,
|
||||||
|
data_file_metadata.len().to_string().parse().unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((StatusCode::OK, headers, body).into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, Some(_)) => Err(Error::DataFileWithoutMetaFile),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_id(id: String) -> String {
|
||||||
|
id.chars()
|
||||||
|
.take_while(|c| *c != '.')
|
||||||
|
.filter(|c| c.is_ascii_alphanumeric())
|
||||||
|
.collect()
|
||||||
|
}
|
35
src/metadata.rs
Normal file
35
src/metadata.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::{
|
||||||
|
fs::File,
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("io error {:?}", 0)]
|
||||||
|
Io(#[from] tokio::io::Error),
|
||||||
|
|
||||||
|
#[error("cbor error: {:?}", 0)]
|
||||||
|
Cbor(#[from] serde_cbor::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Metadata {
|
||||||
|
pub subject: String,
|
||||||
|
pub mimetype: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metadata {
|
||||||
|
pub async fn from_file(path: &str, id: &str) -> Result<Self, Error> {
|
||||||
|
let mut metadata_file = File::open(&format!("{}/{}.meta", path, id)).await?;
|
||||||
|
let mut metadata = Vec::new();
|
||||||
|
metadata_file.read_to_end(&mut metadata).await.unwrap();
|
||||||
|
Ok(serde_cbor::from_slice(&metadata)?)
|
||||||
|
}
|
||||||
|
pub async fn to_file(&self, path: &str, id: &str) -> Result<(), Error> {
|
||||||
|
let metadata = serde_cbor::to_vec(self).unwrap();
|
||||||
|
let mut metadata_file = File::create(&format!("{}/{}.meta", path, id)).await?;
|
||||||
|
metadata_file.write_all(&metadata).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
266
src/openid.rs
Normal file
266
src/openid.rs
Normal file
|
@ -0,0 +1,266 @@
|
||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
};
|
||||||
|
use openidconnect::{
|
||||||
|
core::{CoreAuthenticationFlow, CoreClient, CoreErrorResponseType, CoreProviderMetadata},
|
||||||
|
reqwest::async_http_client,
|
||||||
|
url::ParseError,
|
||||||
|
AccessTokenHash, AuthorizationCode, ClaimsVerificationError, ClientId, ClientSecret, CsrfToken,
|
||||||
|
DiscoveryError, IssuerUrl, Nonce, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier,
|
||||||
|
RedirectUrl, RequestTokenError, Scope, SigningError, StandardErrorResponse, TokenResponse,
|
||||||
|
};
|
||||||
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{metadata::Metadata, AppState};
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("discovery error: {:?}", 0)]
|
||||||
|
Discovery(#[from] DiscoveryError<openidconnect::reqwest::Error<reqwest::Error>>),
|
||||||
|
#[error("parse error: {:?}", 0)]
|
||||||
|
Parse(#[from] ParseError),
|
||||||
|
#[error("request token error: {:?}", 0)]
|
||||||
|
RequestToken(
|
||||||
|
#[from]
|
||||||
|
RequestTokenError<
|
||||||
|
openidconnect::reqwest::Error<reqwest::Error>,
|
||||||
|
StandardErrorResponse<CoreErrorResponseType>,
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
#[error("claims verification error: {:?}", 0)]
|
||||||
|
ClaimsVerification(#[from] ClaimsVerificationError),
|
||||||
|
#[error("signing error: {:?}", 0)]
|
||||||
|
SigningError(#[from] SigningError),
|
||||||
|
#[error("id token not found")]
|
||||||
|
IdTokenNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for Error {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
println!("openid error: {:?}", self);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "internal server error").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct OidcBody {
|
||||||
|
pub code: String,
|
||||||
|
pub state: String,
|
||||||
|
pub session_state: String,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Login {
|
||||||
|
csrf_token: String,
|
||||||
|
nonce: String,
|
||||||
|
pkce_verifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_callback(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(auth_id): Path<String>,
|
||||||
|
Query(body): Query<OidcBody>,
|
||||||
|
) -> Result<impl IntoResponse, Error> {
|
||||||
|
let auth_instance = {
|
||||||
|
let logins = state.logins.lock().await;
|
||||||
|
logins.get(&auth_id).cloned().unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = create_oidc_client(
|
||||||
|
state.issuer.clone(),
|
||||||
|
state.client_id.clone(),
|
||||||
|
state.client_secret.clone(),
|
||||||
|
&state.application_base,
|
||||||
|
&auth_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if auth_instance.csrf_token != body.state {
|
||||||
|
return Ok((StatusCode::BAD_REQUEST, "csrf token is invalid").into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pkce_verifier = PkceCodeVerifier::new(auth_instance.pkce_verifier.clone());
|
||||||
|
let nonce = Nonce::new(auth_instance.nonce.clone());
|
||||||
|
|
||||||
|
let token_response = client
|
||||||
|
.exchange_code(AuthorizationCode::new(body.code.to_string()))
|
||||||
|
// Set the PKCE code verifier.
|
||||||
|
.set_pkce_verifier(pkce_verifier)
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Extract the ID token claims after verifying its authenticity and nonce.
|
||||||
|
let id_token = token_response.id_token().ok_or(Error::IdTokenNotFound)?;
|
||||||
|
let claims = id_token.claims(&client.id_token_verifier(), &nonce)?;
|
||||||
|
|
||||||
|
// Verify the access token hash to ensure that the access token hasn't been substituted for
|
||||||
|
// another user's.
|
||||||
|
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||||
|
let actual_access_token_hash =
|
||||||
|
AccessTokenHash::from_token(token_response.access_token(), &id_token.signing_alg()?)?;
|
||||||
|
if actual_access_token_hash != *expected_access_token_hash {
|
||||||
|
return Ok((StatusCode::BAD_REQUEST, "access token hash is invalid").into_response());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//let mut oidc_user = oidc_user::Entity::find()
|
||||||
|
// .filter(
|
||||||
|
// oidc_user::Column::OidcClientId
|
||||||
|
// .eq(oidc_client.id)
|
||||||
|
// .and(oidc_user::Column::Subject.eq(claims.subject().as_str())),
|
||||||
|
// )
|
||||||
|
// .one(&state.db)
|
||||||
|
// .await?
|
||||||
|
// .map(|x| x.into_active_model())
|
||||||
|
// .unwrap_or_default();
|
||||||
|
//oidc_user.oidc_client_id = ActiveValue::Set(oidc_client.id);
|
||||||
|
//oidc_user.subject = ActiveValue::Set(claims.subject().to_string());
|
||||||
|
//oidc_user.email = ActiveValue::Set(claims.email().map(|x| x.to_string()).unwrap_or_default());
|
||||||
|
//oidc_user.username = ActiveValue::Set(
|
||||||
|
// claims
|
||||||
|
// .preferred_username()
|
||||||
|
// .map(|x| x.to_string())
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
//);
|
||||||
|
//oidc_user.given_name = ActiveValue::Set(
|
||||||
|
// claims
|
||||||
|
// .given_name()
|
||||||
|
// .and_then(|x| x.get(None).map(|x| x.to_string()))
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
//);
|
||||||
|
//oidc_user.middle_name = ActiveValue::Set(
|
||||||
|
// claims
|
||||||
|
// .middle_name()
|
||||||
|
// .and_then(|x| x.get(None).map(|x| x.to_string()))
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
//);
|
||||||
|
//oidc_user.family_name = ActiveValue::Set(
|
||||||
|
// claims
|
||||||
|
// .family_name()
|
||||||
|
// .and_then(|x| x.get(None).map(|x| x.to_string()))
|
||||||
|
// .unwrap_or_default(),
|
||||||
|
//);
|
||||||
|
//oidc_user.locale = ActiveValue::Set(claims.locale().map(|x| x.to_string()).unwrap_or_default());
|
||||||
|
//oidc_user.zoneinfo =
|
||||||
|
// ActiveValue::Set(claims.zoneinfo().map(|x| x.to_string()).unwrap_or_default());
|
||||||
|
|
||||||
|
//let oidc_user = if oidc_user.id.is_unchanged() {
|
||||||
|
// oidc_user.update(&state.db).await?
|
||||||
|
//} else {
|
||||||
|
// oidc_user.insert(&state.db).await?
|
||||||
|
//};
|
||||||
|
|
||||||
|
//let instance = if form.multiple_submissions == 0 {
|
||||||
|
// DatabaseInstance::from_userid(
|
||||||
|
// state.db.clone(),
|
||||||
|
// state.submit_producer.clone(),
|
||||||
|
// oidc_user.id,
|
||||||
|
// form.id,
|
||||||
|
// )
|
||||||
|
// .await?
|
||||||
|
//} else {
|
||||||
|
// None
|
||||||
|
//};
|
||||||
|
|
||||||
|
//let instance = match instance {
|
||||||
|
// Some(x) => x,
|
||||||
|
// None => {
|
||||||
|
// DatabaseInstance::new(
|
||||||
|
// state.db.clone(),
|
||||||
|
// state.submit_producer.clone(),
|
||||||
|
// form.id,
|
||||||
|
// Some(oidc_user.id),
|
||||||
|
// )
|
||||||
|
// .await?
|
||||||
|
// }
|
||||||
|
//};
|
||||||
|
|
||||||
|
{
|
||||||
|
state.logins.lock().await.remove(&auth_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let subject = claims.subject().to_string();
|
||||||
|
|
||||||
|
let id = rand::thread_rng()
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.take(16)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let metadata = Metadata {
|
||||||
|
subject,
|
||||||
|
mimetype: None,
|
||||||
|
};
|
||||||
|
metadata.to_file(&state.path, &id).await.unwrap();
|
||||||
|
|
||||||
|
Ok((Redirect::temporary(&format!("{}{}", state.application_base, id))).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_login(State(state): State<AppState>) -> Result<impl IntoResponse, Error> {
|
||||||
|
let auth_id = rand::thread_rng()
|
||||||
|
.sample_iter(Alphanumeric)
|
||||||
|
.take(32)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let client = create_oidc_client(
|
||||||
|
state.issuer.clone(),
|
||||||
|
state.client_id.clone(),
|
||||||
|
state.client_secret.clone(),
|
||||||
|
&state.application_base,
|
||||||
|
&auth_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
|
||||||
|
let (auth_url, csrf_token, nonce) = {
|
||||||
|
let mut auth = client.authorize_url(
|
||||||
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
|
CsrfToken::new_random,
|
||||||
|
Nonce::new_random,
|
||||||
|
);
|
||||||
|
|
||||||
|
for scope in state.scopes.iter() {
|
||||||
|
auth = auth.add_scope(Scope::new(scope.to_string()));
|
||||||
|
}
|
||||||
|
auth.set_pkce_challenge(pkce_challenge).url()
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut logins = state.logins.lock().await;
|
||||||
|
logins.insert(
|
||||||
|
auth_id,
|
||||||
|
Login {
|
||||||
|
csrf_token: csrf_token.secret().to_string(),
|
||||||
|
nonce: nonce.secret().to_string(),
|
||||||
|
pkce_verifier: pkce_verifier.secret().to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Redirect::temporary(auth_url.as_str()).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_oidc_client(
|
||||||
|
issuer: String,
|
||||||
|
client_id: String,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
application_base: &str,
|
||||||
|
auth_id: &str,
|
||||||
|
) -> Result<CoreClient, Error> {
|
||||||
|
let provider_metadata =
|
||||||
|
CoreProviderMetadata::discover_async(IssuerUrl::new(issuer)?, async_http_client).await?;
|
||||||
|
let client = CoreClient::from_provider_metadata(
|
||||||
|
provider_metadata,
|
||||||
|
ClientId::new(client_id.clone()),
|
||||||
|
client_secret.map(ClientSecret::new),
|
||||||
|
)
|
||||||
|
.set_redirect_uri(RedirectUrl::new(format!(
|
||||||
|
"{}/login/{}",
|
||||||
|
application_base, auth_id
|
||||||
|
))?);
|
||||||
|
Ok(client)
|
||||||
|
}
|
Loading…
Reference in a new issue