auths_cli/commands/
auth.rs1use anyhow::{Context, Result, anyhow};
2use clap::{Parser, Subcommand};
3
4use auths_sdk::storage_layout::layout;
5
6use crate::factories::storage::build_auths_context;
7
8use crate::commands::executable::ExecutableCommand;
9use crate::config::CliConfig;
10use crate::ux::format::{JsonResponse, is_json_mode};
11
12#[derive(Parser, Debug, Clone)]
14#[command(
15 about = "Authenticate with external services using your auths identity",
16 after_help = "Examples:
17 auths auth challenge --nonce abc123def456 --domain example.com
18 # Sign an authentication challenge
19 auths auth challenge --nonce abc123def456
20 # Sign challenge for default domain (auths.dev)
21
22Flow:
23 1. Service sends you a nonce
24 2. Run: auths auth challenge --nonce <nonce> --domain <domain>
25 3. Service verifies your signature against your DID
26
27Related:
28 auths id — Manage your identity
29 auths sign — Sign files and commits
30 auths verify — Verify signatures"
31)]
32pub struct AuthCommand {
33 #[clap(subcommand)]
34 pub subcommand: AuthSubcommand,
35}
36
37#[derive(Subcommand, Debug, Clone)]
39pub enum AuthSubcommand {
40 Challenge {
42 #[arg(long)]
44 nonce: String,
45
46 #[arg(long, default_value = "auths.dev")]
48 domain: String,
49 },
50}
51
52fn handle_auth_challenge(nonce: &str, domain: &str, ctx: &CliConfig) -> Result<()> {
53 let repo_path = layout::resolve_repo_path(ctx.repo_path.clone())?;
54 let passphrase_provider = ctx.passphrase_provider.clone();
55
56 let auths_ctx = build_auths_context(
57 &repo_path,
58 &ctx.env_config,
59 Some(ctx.passphrase_provider.clone()),
60 )?;
61 let managed = auths_ctx
62 .identity_storage
63 .load_identity()
64 .context("No identity found. Run `auths init` first.")?;
65
66 let controller_did = &managed.controller_did;
67
68 let key_alias_str =
69 super::key_detect::auto_detect_device_key(ctx.repo_path.as_deref(), &ctx.env_config)?;
70 let key_alias = auths_sdk::keychain::KeyAlias::new(&key_alias_str)
71 .map_err(|e| anyhow!("Invalid key alias: {e}"))?;
72
73 let message = auths_sdk::workflows::auth::build_auth_challenge_message(nonce, domain)
74 .context("Failed to build auth challenge payload")?;
75
76 let (signature_bytes, public_key_bytes, _curve) = auths_sdk::keychain::sign_with_key(
77 auths_ctx.key_storage.as_ref(),
78 &key_alias,
79 passphrase_provider.as_ref(),
80 message.as_bytes(),
81 )
82 .with_context(|| format!("Failed to sign auth challenge with key '{}'", key_alias))?;
83
84 let result = auths_sdk::workflows::auth::SignedAuthChallenge {
85 signature_hex: hex::encode(&signature_bytes),
86 public_key_hex: hex::encode(&public_key_bytes),
87 did: controller_did.to_string(),
88 };
89
90 if is_json_mode() {
91 JsonResponse::success(
92 "auth challenge",
93 &serde_json::json!({
94 "signature": result.signature_hex,
95 "public_key": result.public_key_hex,
96 "did": result.did,
97 }),
98 )
99 .print()
100 .map_err(anyhow::Error::from)
101 } else {
102 println!("Signature: {}", result.signature_hex);
103 println!("Public Key: {}", result.public_key_hex);
104 println!("DID: {}", result.did);
105 Ok(())
106 }
107}
108
109impl ExecutableCommand for AuthCommand {
110 fn execute(&self, ctx: &CliConfig) -> Result<()> {
111 match &self.subcommand {
112 AuthSubcommand::Challenge { nonce, domain } => {
113 handle_auth_challenge(nonce, domain, ctx)
114 }
115 }
116 }
117}