use std::sync::Arc;
use axum::body::Bytes;
use axum::extract::{Form, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::CookieJar;
use axum_extra::extract::cookie::{Cookie, SameSite};
use cookie::time::Duration;
use ordinary_auth::AuthClient;
use ordinary_config::{AuthConfig, ClientPasswordHash};
use serde::Deserialize;
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use sha2::{Digest, Sha256};
fn refresh_token_cookie(
jar: CookieJar,
token: &Bytes,
auth_config: &AuthConfig,
secure: bool,
) -> CookieJar {
let name = if secure {
tracing::info!("generated secure refresh cookie");
"__Secure-ORDINARY-REFRESH-TOKEN"
} else {
tracing::warn!("generated insecure refresh cookie");
"ORDINARY-REFRESH-TOKEN"
};
jar.add(
Cookie::build((name, b64.encode(token)))
.secure(secure)
.http_only(true)
.path("/access/redirect")
.same_site(SameSite::Strict)
.max_age(Duration::seconds(i64::from(
auth_config.refresh_token.lifetime,
))),
)
}
pub async fn start(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
body: Bytes,
) -> impl IntoResponse {
let span = tracing::info_span!("auth", flv = %"wasm");
span.in_scope(|| match state.auth.login_start(body) {
Ok(v) => (StatusCode::OK, v),
Err(e) => {
tracing::error!("{e}");
(StatusCode::INTERNAL_SERVER_ERROR, Bytes::new())
}
})
}
pub async fn finish(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
body: Bytes,
) -> impl IntoResponse {
let span = tracing::info_span!("auth", flv = %"wasm");
span.in_scope(|| match state.auth.login_finish(body, true) {
Ok((token, _, _)) => (StatusCode::OK, token),
Err(e) => {
tracing::error!("{e}");
(StatusCode::INTERNAL_SERVER_ERROR, Bytes::new())
}
})
}
#[derive(Deserialize, Debug)]
pub struct LoginForm {
account: String,
password: String,
mfa_code: String,
}
pub async fn form(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
jar: CookieJar,
Form(login_form): Form<LoginForm>,
) -> Result<(CookieJar, Redirect), StatusCode> {
let span = tracing::info_span!("auth", flv = %"noscript");
span.in_scope(|| {
let account = login_form.account.as_bytes();
if account.len() > 255 {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
let password = login_form.password.as_bytes();
let mfa_code = login_form.mfa_code.as_bytes();
let app_name = state.config.domain.as_bytes();
let mut input = app_name.to_vec();
input.extend_from_slice(account);
let mut password_input = input.clone();
let mut mfa_input = input.clone();
password_input.extend_from_slice(password);
mfa_input.extend_from_slice(mfa_code);
let mut password = vec![];
let mut mfa_code = vec![];
if let Some(auth) = &state.config.auth {
match &auth.client_hash {
ClientPasswordHash::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(&password_input);
password = hasher.finalize().to_vec();
let mut hasher = Sha256::new();
hasher.update(&mfa_input);
mfa_code = hasher.finalize().to_vec();
}
}
}
match AuthClient::login_start_req(account, &password[..]) {
Ok((client_state, req)) => match state.auth.login_start(req) {
Ok(server_message) => match AuthClient::login_finish_req(
account,
&password[..],
&mfa_code[..],
&client_state,
&server_message[..],
None,
) {
Ok((req, session_key)) => match state.auth.login_finish(req, true) {
Ok((res, _, _)) => match AuthClient::decrypt_token(&res, &session_key) {
Ok(token) => {
return Ok((
refresh_token_cookie(
jar,
&token,
&state.auth.config,
state.secure_cookies,
),
Redirect::to("/accounts/access/redirect"),
));
}
Err(err) => tracing::error!("{err}"),
},
Err(err) => tracing::error!("{err}"),
},
Err(err) => tracing::error!("{err}"),
},
Err(err) => tracing::error!("{err}"),
},
Err(err) => tracing::error!("{err}"),
}
Err(StatusCode::INTERNAL_SERVER_ERROR)
})
}
pub async fn hash_only(
State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
jar: CookieJar,
body: Bytes,
) -> impl IntoResponse {
let span = tracing::info_span!("auth", flv = %"js");
span.in_scope(|| {
if let Some(account_len) = body.first() {
let account_len = *account_len as usize;
if body.len() < account_len + 1 + 32 + 32 {
return (StatusCode::INTERNAL_SERVER_ERROR, None, Bytes::new());
}
let account = &body[1..=account_len];
let mfa_code = &body[account_len + 1..account_len + 1 + 32];
let password = &body[account_len + 1 + 32..account_len + 1 + 32 + 32];
let mut client_verifier = None;
if body.len() == account_len + 1 + 32 + 32 + 32 && !state.auth.config.cookies_enabled {
client_verifier =
Some(&body[account_len + 1 + 32 + 32..account_len + 1 + 32 + 32 + 32]);
}
match AuthClient::login_start_req(account, password) {
Ok((client_state, req)) => match state.auth.login_start(req) {
Ok(server_message) => match AuthClient::login_finish_req(
account,
password,
mfa_code,
&client_state,
&server_message[..],
client_verifier,
) {
Ok((req, session_key)) => match state.auth.login_finish(req, true) {
Ok((res, _, _)) => {
match AuthClient::decrypt_token(&res, &session_key) {
Ok(token) => {
let cookie = if state.auth.config.cookies_enabled {
Some(refresh_token_cookie(
jar,
&token,
&state.auth.config,
state.secure_cookies,
))
} else {
None
};
return (StatusCode::OK, cookie, token);
}
Err(err) => tracing::error!("{err}"),
}
}
Err(err) => tracing::error!("{err}"),
},
Err(err) => tracing::error!("{err}"),
},
Err(err) => tracing::error!("{err}"),
},
Err(err) => tracing::error!("{err}"),
}
}
(StatusCode::INTERNAL_SERVER_ERROR, None, Bytes::new())
})
}