auth: use a global password for the webapp

This commit is contained in:
Armaël Guéneau 2025-02-07 16:59:23 +01:00
parent 6342620f70
commit 7bd511eaae
8 changed files with 51 additions and 46 deletions

7
Cargo.lock generated
View file

@ -874,6 +874,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "convert_case"
version = "0.4.0"
@ -1202,6 +1208,7 @@ dependencies = [
"aws-config",
"aws-sdk-s3",
"axum",
"constant_time_eq",
"forgejo-api",
"include_dir",
"lazy_static",

View file

@ -29,6 +29,7 @@ tower = "0.5.2"
tower-http = { version = "0.6.2", features = ["trace"] }
tracing-subscriber = "0.3"
tracing = "0.1.41"
constant_time_eq = "0.3.1"
[profile.profiling]
inherits = "dev"

View file

@ -16,9 +16,11 @@
Forgery reads the following environment variables:
- `FORGE_URL` (**mandatory**): url of the forgejo instance (e.g.
https://git.deuxfleurs.fr)
- `FORGE_API_TOKEN` (**mandatory**): Forgejo API token *granting admin access*.
You can generate an API token using the Forgejo web interface in `Settings ->
Applications -> Generate New Token`.
- `FORGE_API_TOKEN` (**mandatory**): Forgejo API token *granting read/write
access* to admin rights, users, issues and repos. You can generate an API
token using the Forgejo web interface in `Settings -> Applications -> Generate
New Token`.
- `PASSWORD` (**mandatory**): password required to authenticate to forgery.
- `ACTUALLY_BAN_USERS` (default: `false`): define it to `true` to actually lock
user accounts, send notification emails and eventually delete user accounts.
Otherwise, no actual action is taken: spammers are only listed in the
@ -57,7 +59,6 @@ Environment variables read when `STORAGE_BACKEND=s3`:
(Current behavior is to periodically retry, avoid deleting if the account
could not be locked, but delete the account after the grace period even if
the email could not be sent…)
- auth: add support for connecting to the forge using oauth?
- improve error handling? currently the app will panic if writing to the storage
backend fails. Can we do better?
- error reporting when the auth token is not working/outdated and background jobs fail

File diff suppressed because one or more lines are too long

View file

@ -62,6 +62,7 @@ const GUESS_LEGIT_THRESHOLD: f32 = 0.3;
pub struct Config {
pub forge_url: Url,
pub forge_api_token: String,
pub password: String,
pub actually_ban: ActuallyBan,
pub bind_addr: String,
}
@ -70,6 +71,7 @@ impl Config {
async fn from_env() -> anyhow::Result<Self> {
let forge_url_s = env_var("FORGE_URL")?;
let forge_api_token = env_var("FORGE_API_TOKEN")?;
let password = env_var("PASSWORD")?;
let actually_ban = match env_var("ACTUALLY_BAN_USERS").as_deref() {
Ok("true") => ActuallyBan::Yes {
@ -91,6 +93,7 @@ impl Config {
Ok(Config {
forge_url: Url::parse(&forge_url_s).context("parsing FORGE_URL")?,
forge_api_token,
password,
actually_ban,
bind_addr,
})
@ -441,15 +444,19 @@ async fn get_auth(State(data): State<Arc<AppState>>) -> Result<Html<String>, App
#[derive(Deserialize)]
struct AuthForm {
token: String,
password: String,
}
async fn post_auth(session: Session, Form(form): Form<AuthForm>) -> impl IntoResponse {
session
.insert(session::TOKEN_KEY, &form.token)
.await
.unwrap();
(StatusCode::SEE_OTHER, [(header::LOCATION, "/")])
async fn post_auth(State(data): State<Arc<AppState>>, session: Session, Form(form): Form<AuthForm>) -> impl IntoResponse {
let p1 = form.password.as_bytes();
let p2 = data.config.password.as_bytes();
if p1.len() == p2.len() && constant_time_eq::constant_time_eq(p1, p2) {
session::set_authenticated(&session).await;
(StatusCode::SEE_OTHER, [(header::LOCATION, "/")])
} else {
session.flush().await.unwrap();
(StatusCode::SEE_OTHER, [(header::LOCATION, "/auth")])
}
}
async fn post_logout(session: Session) -> impl IntoResponse {

View file

@ -185,6 +185,7 @@ pub async fn get_user_data(forge: &Forgejo) -> anyhow::Result<HashMap<UserId, Us
));
}
info!("done fetching user data from forgejo");
// discard users with an entirely empty profile: there is nothing useful we
// can say about them
let data = data

View file

@ -2,13 +2,17 @@ use axum::extract::FromRequestParts;
use axum::http::{header, request::Parts, HeaderMap, HeaderValue, StatusCode};
use tower_sessions::Session;
#[allow(dead_code)] // TODO: WIP
pub struct AuthenticatedUser {
session: Session,
api_token: String,
pub struct AuthenticatedUser(());
const IS_AUTH_KEY: &str = "IS_AUTH";
pub async fn set_authenticated(session: &Session) {
session.insert(IS_AUTH_KEY, true).await.unwrap()
}
pub const TOKEN_KEY: &str = "API_TOKEN";
pub async fn is_authenticated(session: &Session) -> bool {
matches!(session.get(IS_AUTH_KEY).await.unwrap(), Some(true))
}
impl<S> FromRequestParts<S> for AuthenticatedUser
where
@ -22,25 +26,17 @@ where
Err((status, body)) => return Err((status, HeaderMap::new(), body.to_string())),
};
let api_token: Option<String> = session.get(TOKEN_KEY).await.unwrap(); // TODO: is it safe to [unwrap] here?
match api_token {
Some(api_token) => Ok(Self { session, api_token }),
None => {
// Unclear that this case can actually happen, but in doubt,
// clear the session and force to re-auth.
// It is safe to [unwrap] the result here because we use
// [MemoryStore] as the underlying store, on which [delete]
// never fails.
session.flush().await.unwrap();
Err((
StatusCode::SEE_OTHER,
HeaderMap::from_iter(
[(header::LOCATION, HeaderValue::from_static("/auth"))].into_iter(),
),
"client is not authenticated, redirecting to /auth".to_string(),
))
}
if is_authenticated(&session).await {
Ok(Self(()))
} else {
session.flush().await.unwrap();
Err((
StatusCode::SEE_OTHER,
HeaderMap::from_iter(
[(header::LOCATION, HeaderValue::from_static("/auth"))].into_iter(),
),
"client is not authenticated, redirecting to /auth".to_string(),
))
}
}
}

View file

@ -4,17 +4,9 @@
{% block content %}
<h1>Authentication required</h1>
<p>
To obtain a suitable API token, you must login to Forgejo using an
administrator account and go to <a href="{{forge_url}}user/settings/applications">{{forge_url}}user/settings/applications</a>.
Then, create an API token with access to all organizations and repos, allowing
read and writet access to: "admin", "misc", "organization", "issue", "repository" and "user". Store
the resulting token safely (e.g. in your password manager).
</p>
<form method="post">
<label for="token">Forgejo API token:</label>
<input id="token" name="token" type="password" required/>
<label for="password">Password:</label>
<input id="password" name="password" type="password" required/>
<input type="submit" value="Submit" class="button"/>
</form>