Skip to main content

auths_cli/commands/
auth.rs

1use 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/// Authenticate with external services using your auths identity.
13#[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/// Subcommands for authentication operations.
38#[derive(Subcommand, Debug, Clone)]
39pub enum AuthSubcommand {
40    /// Sign an authentication challenge for DID-based login
41    Challenge {
42        /// The challenge nonce from the authentication server
43        #[arg(long)]
44        nonce: String,
45
46        /// The domain requesting authentication
47        #[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}