use axum_extra::extract::cookie::Key;
use url::Url;
use super::error::AuthError;
use crate::oauth::{AuthClient, OAuthConfig};
const DEFAULT_SESSION_COOKIE_NAME: &str = "__ppoppo_session";
const DEFAULT_SESSION_TTL_DAYS: i64 = 30;
const DEFAULT_AUTH_PATH: &str = "/api/auth";
const DEFAULT_ERROR_REDIRECT: &str = "/login";
#[derive(Clone)]
pub(crate) struct AuthSettings {
pub(crate) cookie_key: Key,
pub(crate) session_cookie_name: String,
pub(crate) session_ttl_days: i64,
pub(crate) secure_cookies: bool,
pub(crate) auth_path: String,
pub(crate) login_redirect: String,
pub(crate) logout_redirect: String,
pub(crate) error_redirect: String,
pub(crate) dev_login_enabled: bool,
}
impl AuthSettings {
fn defaults() -> Self {
Self {
cookie_key: Key::generate(),
session_cookie_name: DEFAULT_SESSION_COOKIE_NAME.into(),
session_ttl_days: DEFAULT_SESSION_TTL_DAYS,
secure_cookies: true,
auth_path: DEFAULT_AUTH_PATH.into(),
login_redirect: "/".into(),
logout_redirect: "/".into(),
error_redirect: DEFAULT_ERROR_REDIRECT.into(),
dev_login_enabled: false,
}
}
}
pub struct PasAuthConfig {
pub(super) client: AuthClient,
pub(super) settings: AuthSettings,
}
impl PasAuthConfig {
#[must_use]
pub fn new(client_id: impl Into<String>, redirect_uri: Url) -> Self {
let config = OAuthConfig::new(client_id, redirect_uri);
Self {
client: AuthClient::new(config),
settings: AuthSettings::defaults(),
}
}
#[must_use]
pub fn from_client(client: AuthClient) -> Self {
Self {
client,
settings: AuthSettings::defaults(),
}
}
pub fn from_env() -> Result<Self, AuthError> {
let client_id = std::env::var("PAS_CLIENT_ID")
.map_err(|_| AuthError::Config("PAS_CLIENT_ID is required".into()))?;
let redirect_uri_str = std::env::var("PAS_REDIRECT_URI")
.map_err(|_| AuthError::Config("PAS_REDIRECT_URI is required".into()))?;
let redirect_uri: Url = redirect_uri_str
.parse()
.map_err(|e| AuthError::Config(format!("PAS_REDIRECT_URI: {e}")))?;
let mut config = OAuthConfig::new(client_id, redirect_uri);
if let Some(url) = parse_optional_url_env("PAS_AUTH_URL")? {
config = config.with_auth_url(url);
}
if let Some(url) = parse_optional_url_env("PAS_TOKEN_URL")? {
config = config.with_token_url(url);
}
if let Some(url) = parse_optional_url_env("PAS_USERINFO_URL")? {
config = config.with_userinfo_url(url);
}
if let Ok(scopes) = std::env::var("PAS_SCOPES") {
config =
config.with_scopes(scopes.split(',').map(|s| s.trim().to_string()).collect());
}
let dev_auth = matches!(
std::env::var("DEV_AUTH").as_deref(),
Ok("1") | Ok("true"),
);
let cookie_key = match std::env::var("COOKIE_KEY") {
Ok(k) => Key::try_from(k.as_bytes()).map_err(|_| {
AuthError::Config(
"COOKIE_KEY is set but invalid (must be at least 64 bytes). \
Remove the env var to use an ephemeral key, or provide a valid key."
.into(),
)
})?,
Err(_) => {
tracing::warn!(
"COOKIE_KEY not set — using ephemeral key. \
All sessions will be invalidated on restart. \
Set COOKIE_KEY to a stable 64+ byte value in production."
);
Key::generate()
}
};
if dev_auth {
tracing::warn!(
"DEV_AUTH is enabled — dev-login route is active and secure cookies are disabled. \
This MUST NOT be used in production."
);
}
Ok(Self::from_client(AuthClient::new(config))
.with_cookie_key(cookie_key)
.with_secure_cookies(!dev_auth)
.with_dev_login_enabled(dev_auth))
}
#[must_use]
pub fn with_cookie_key(mut self, key: Key) -> Self {
self.settings.cookie_key = key;
self
}
#[must_use]
pub fn with_session_cookie_name(mut self, name: impl Into<String>) -> Self {
self.settings.session_cookie_name = name.into();
self
}
#[must_use]
pub fn with_session_ttl_days(mut self, days: i64) -> Self {
self.settings.session_ttl_days = days;
self
}
#[must_use]
pub fn with_secure_cookies(mut self, secure: bool) -> Self {
self.settings.secure_cookies = secure;
self
}
#[must_use]
pub fn with_auth_path(mut self, path: impl Into<String>) -> Self {
self.settings.auth_path = path.into();
self
}
#[must_use]
pub fn with_login_redirect(mut self, path: impl Into<String>) -> Self {
self.settings.login_redirect = path.into();
self
}
#[must_use]
pub fn with_logout_redirect(mut self, path: impl Into<String>) -> Self {
self.settings.logout_redirect = path.into();
self
}
#[must_use]
pub fn with_error_redirect(mut self, path: impl Into<String>) -> Self {
self.settings.error_redirect = path.into();
self
}
#[must_use]
pub fn with_dev_login_enabled(mut self, enabled: bool) -> Self {
self.settings.dev_login_enabled = enabled;
self
}
}
fn parse_optional_url_env(key: &str) -> Result<Option<Url>, AuthError> {
match std::env::var(key) {
Ok(s) => {
let url: Url = s.parse().map_err(|e| AuthError::Config(format!("{key}: {e}")))?;
Ok(Some(url))
}
Err(_) => Ok(None),
}
}