mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
fix(auth): clarify dev-token login recovery
Format the dev-token-only login failure as an action block and include the resolved --server value in the recovery command.
This commit is contained in:
parent
843ef3c6a9
commit
d58c45fe80
2 changed files with 152 additions and 16 deletions
|
|
@ -7,6 +7,7 @@ use fabro_http::header::CONTENT_TYPE;
|
|||
use fabro_util::browser;
|
||||
use fabro_util::dev_token::validate_dev_token_format;
|
||||
use fabro_util::printer::Printer;
|
||||
use fabro_util::terminal::Styles;
|
||||
use serde::Deserialize;
|
||||
use tokio::time::timeout;
|
||||
|
||||
|
|
@ -90,9 +91,15 @@ pub(super) async fn login_command(args: AuthLoginArgs, base_ctx: &CommandContext
|
|||
let code = match callback {
|
||||
Ok(success) => success.code,
|
||||
Err(failure) => {
|
||||
let styles = Styles::detect_stderr();
|
||||
bail!(
|
||||
"{}",
|
||||
login_failure_message(&failure.error_code, Some(&failure.error_description))
|
||||
login_failure_message(
|
||||
&failure.error_code,
|
||||
Some(&failure.error_description),
|
||||
&target,
|
||||
&styles,
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -224,14 +231,17 @@ fn open_browser_or_print(browser_url: &str, no_browser: bool, printer: Printer)
|
|||
}
|
||||
}
|
||||
|
||||
fn login_failure_message(error_code: &str, error_description: Option<&str>) -> String {
|
||||
fn login_failure_message(
|
||||
error_code: &str,
|
||||
error_description: Option<&str>,
|
||||
target: &ServerTarget,
|
||||
styles: &Styles,
|
||||
) -> String {
|
||||
match error_code {
|
||||
"github_session_required" => {
|
||||
"GitHub session required. Complete sign-in in the browser and try again.".to_string()
|
||||
}
|
||||
"github_not_configured" => {
|
||||
"This server uses dev-token auth. Copy the token from the server and run: `fabro auth login --dev-token <TOKEN>`".to_string()
|
||||
}
|
||||
"github_not_configured" => dev_token_auth_required_message_with_styles(target, styles),
|
||||
"access_denied" => "Authorization denied.".to_string(),
|
||||
"unauthorized" => "Login not permitted.".to_string(),
|
||||
"server_error" => error_description
|
||||
|
|
@ -245,6 +255,14 @@ fn login_failure_message(error_code: &str, error_description: Option<&str>) -> S
|
|||
}
|
||||
}
|
||||
|
||||
fn dev_token_auth_required_message_with_styles(target: &ServerTarget, styles: &Styles) -> String {
|
||||
let command = format!("fabro auth login --server {target} --dev-token <TOKEN>");
|
||||
let command = styles.bold_cyan.apply_to(command);
|
||||
format!(
|
||||
"This server uses dev-token auth.\n\nFind the dev token:\n - In the server terminal output\n - In the install output\n - For file-based installs, in `server.dev-token` under the configured server storage directory\n\nThen run:\n {command}"
|
||||
)
|
||||
}
|
||||
|
||||
fn identity_summary(subject: &StoredSubject) -> String {
|
||||
if !subject.name.is_empty() && !subject.email.is_empty() {
|
||||
format!("{} ({} <{}>)", subject.login, subject.name, subject.email)
|
||||
|
|
@ -266,10 +284,14 @@ struct OAuthErrorBody {
|
|||
mod tests {
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use fabro_util::terminal::Styles;
|
||||
use insta::assert_snapshot;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use super::{browser_origin, build_browser_url, login_failure_message};
|
||||
use super::{
|
||||
browser_origin, build_browser_url, dev_token_auth_required_message_with_styles,
|
||||
login_failure_message,
|
||||
};
|
||||
use crate::user_config::ServerTarget;
|
||||
|
||||
#[test]
|
||||
|
|
@ -294,31 +316,77 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn login_failure_messages_render_known_server_codes() {
|
||||
let target = ServerTarget::http_url("http://127.0.0.1:32276").unwrap();
|
||||
let styles = Styles::new(false);
|
||||
assert_eq!(
|
||||
login_failure_message("github_session_required", Some("GitHub session required")),
|
||||
login_failure_message(
|
||||
"github_session_required",
|
||||
Some("GitHub session required"),
|
||||
&target,
|
||||
&styles
|
||||
),
|
||||
"GitHub session required. Complete sign-in in the browser and try again."
|
||||
);
|
||||
assert_eq!(
|
||||
login_failure_message("access_denied", Some("Authorization denied")),
|
||||
login_failure_message(
|
||||
"access_denied",
|
||||
Some("Authorization denied"),
|
||||
&target,
|
||||
&styles
|
||||
),
|
||||
"Authorization denied."
|
||||
);
|
||||
assert_eq!(
|
||||
login_failure_message("unauthorized", Some("Login not permitted")),
|
||||
login_failure_message(
|
||||
"unauthorized",
|
||||
Some("Login not permitted"),
|
||||
&target,
|
||||
&styles
|
||||
),
|
||||
"Login not permitted."
|
||||
);
|
||||
assert_eq!(
|
||||
login_failure_message(
|
||||
"github_not_configured",
|
||||
Some("GitHub authentication is not enabled on this server")
|
||||
Some("GitHub authentication is not enabled on this server"),
|
||||
&target,
|
||||
&styles
|
||||
),
|
||||
"This server uses dev-token auth. Copy the token from the server and run: `fabro auth login --dev-token <TOKEN>`"
|
||||
"This server uses dev-token auth.\n\nFind the dev token:\n - In the server terminal output\n - In the install output\n - For file-based installs, in `server.dev-token` under the configured server storage directory\n\nThen run:\n fabro auth login --server http://127.0.0.1:32276 --dev-token <TOKEN>"
|
||||
);
|
||||
assert_eq!(
|
||||
login_failure_message("server_error", Some("SESSION_SECRET is not configured")),
|
||||
login_failure_message(
|
||||
"server_error",
|
||||
Some("SESSION_SECRET is not configured"),
|
||||
&target,
|
||||
&styles
|
||||
),
|
||||
"SESSION_SECRET is not configured"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dev_token_auth_required_message_formats_recovery_steps() {
|
||||
let target = ServerTarget::http_url("http://127.0.0.1:32276").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dev_token_auth_required_message_with_styles(&target, &Styles::new(false)),
|
||||
"This server uses dev-token auth.\n\nFind the dev token:\n - In the server terminal output\n - In the install output\n - For file-based installs, in `server.dev-token` under the configured server storage directory\n\nThen run:\n fabro auth login --server http://127.0.0.1:32276 --dev-token <TOKEN>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dev_token_auth_required_message_colors_example_command_when_enabled() {
|
||||
let target = ServerTarget::http_url("http://127.0.0.1:32276").unwrap();
|
||||
let message = dev_token_auth_required_message_with_styles(&target, &Styles::new(true));
|
||||
|
||||
assert!(message.contains("\x1b["));
|
||||
assert!(
|
||||
message
|
||||
.contains("fabro auth login --server http://127.0.0.1:32276 --dev-token <TOKEN>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_login_accepts_http_target() {
|
||||
let target = ServerTarget::http_url("http://fabro.example.com/api/v1").unwrap();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,16 @@ const ACCESS_TOKEN_TTL_MINUTES: i64 = 10;
|
|||
const REFRESH_TOKEN_TTL_DAYS: i64 = 30;
|
||||
const REFRESH_TOKEN_PREFIX: &str = "fabro_refresh_";
|
||||
const GITHUB_NOT_CONFIGURED: &str = "GitHub login is not configured for this server.";
|
||||
const DEV_TOKEN_LOGIN_INSTRUCTIONS: &str = concat!(
|
||||
"This server uses dev-token auth.\n\n",
|
||||
"Find the dev token:\n",
|
||||
" - In the server terminal output\n",
|
||||
" - In the install output\n",
|
||||
" - For file-based installs, in `server.dev-token` under the configured server storage ",
|
||||
"directory\n\n",
|
||||
"Then run:\n",
|
||||
" fabro auth login --server <SERVER> --dev-token <TOKEN>"
|
||||
);
|
||||
const INVALID_REDIRECT_URI: &str = "The provided redirect URI is not valid for CLI login.";
|
||||
const INVALID_OR_MISSING_STATE: &str = "The login state is missing or invalid.";
|
||||
const MISSING_FLOW_COOKIE: &str = "Your login session has expired. Please start again.";
|
||||
|
|
@ -131,7 +141,7 @@ async fn start(
|
|||
&redirect_uri,
|
||||
state_token,
|
||||
"github_not_configured",
|
||||
"This server uses dev-token auth. Copy the token from the server and run: `fabro auth login --dev-token <TOKEN>`",
|
||||
DEV_TOKEN_LOGIN_INSTRUCTIONS,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1138,8 +1148,8 @@ mod tests {
|
|||
use uuid::Uuid;
|
||||
|
||||
use super::{
|
||||
CliFlowCookie, add_cli_flow_cookie, read_private_cli_flow, user_agent_fingerprint,
|
||||
web_routes,
|
||||
CliFlowCookie, DEV_TOKEN_LOGIN_INSTRUCTIONS, add_cli_flow_cookie, read_private_cli_flow,
|
||||
user_agent_fingerprint, web_routes,
|
||||
};
|
||||
use crate::auth::{self, AuthCode, RefreshToken};
|
||||
use crate::jwt_auth::{AuthMode, ConfiguredAuth};
|
||||
|
|
@ -1161,6 +1171,16 @@ mod tests {
|
|||
AuthMode::Enabled(config)
|
||||
}
|
||||
|
||||
fn dev_token_auth_mode() -> AuthMode {
|
||||
AuthMode::Enabled(ConfiguredAuth::new(
|
||||
vec![ServerAuthMethod::DevToken],
|
||||
Some(
|
||||
"fabro_dev_abababababababababababababababababababababababababababababababab"
|
||||
.to_string(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn github_settings(web_url: &str) -> fabro_types::ServerSettings {
|
||||
ServerSettingsBuilder::from_toml(&format!(
|
||||
r#"
|
||||
|
|
@ -1185,6 +1205,13 @@ client_id = "github-client-id"
|
|||
|
||||
fn test_router(
|
||||
settings: fabro_types::ServerSettings,
|
||||
) -> (axum::Router, Arc<crate::server::AppState>) {
|
||||
test_router_with_auth_mode(settings, github_auth_mode())
|
||||
}
|
||||
|
||||
fn test_router_with_auth_mode(
|
||||
settings: fabro_types::ServerSettings,
|
||||
auth_mode: AuthMode,
|
||||
) -> (axum::Router, Arc<crate::server::AppState>) {
|
||||
let state = server::create_test_app_state_with_runtime_settings_and_session_key(
|
||||
settings,
|
||||
|
|
@ -1193,7 +1220,7 @@ client_id = "github-client-id"
|
|||
);
|
||||
let app = axum::Router::new()
|
||||
.nest("/auth", web_routes())
|
||||
.layer(Extension(github_auth_mode()))
|
||||
.layer(Extension(auth_mode))
|
||||
.with_state(Arc::clone(&state));
|
||||
(app, state)
|
||||
}
|
||||
|
|
@ -1369,6 +1396,47 @@ client_id = "github-client-id"
|
|||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_without_github_auth_redirects_with_dev_token_instructions() {
|
||||
let (app, _state) = test_router_with_auth_mode(
|
||||
github_settings("https://fabro.example"),
|
||||
dev_token_auth_mode(),
|
||||
);
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/auth/cli/start?redirect_uri=http://127.0.0.1:4444/callback&state=abcdefghijklmnop&code_challenge=challenge&code_challenge_method=S256")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
||||
let location = response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.expect("redirect location should be present");
|
||||
let url = url::Url::parse(location).expect("location should be a valid URL");
|
||||
let query = url
|
||||
.query_pairs()
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
assert_eq!(
|
||||
query.get("error").map(std::borrow::Cow::as_ref),
|
||||
Some("github_not_configured")
|
||||
);
|
||||
assert_eq!(
|
||||
query.get("error_description").map(std::borrow::Cow::as_ref),
|
||||
Some(DEV_TOKEN_LOGIN_INSTRUCTIONS)
|
||||
);
|
||||
assert_eq!(
|
||||
query.get("state").map(std::borrow::Cow::as_ref),
|
||||
Some("abcdefghijklmnop")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_with_github_session_sets_flow_cookie_and_redirects_to_resume() {
|
||||
let key = test_cookie_key();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue