feat(auth): idempotent auth login — verify existing token when no --token provided
- commit
- 925cad780972ed6f7b0ea84d579b80c037994647
- author
- brice · · 2026-03-23 05:32 UTC
- parent
- 7d67af7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +33 -7
M internal/adapters/in/cli/auth.go +33 -7
| @@ -211,21 +211,22 @@ func newAuthLoginCmd() *cobra.Command { | |||
| 211 | 211 | cmd := &cobra.Command{ | |
| 212 | 212 | Use: "login", | |
| 213 | 213 | Short: "Authenticate with a Gordon server", | |
| 214 | - | Long: `Store a pre-generated token for a Gordon server remote. | |
| 214 | + | Long: `Store or verify a token for a Gordon server remote. | |
| 215 | 215 | ||
| 216 | - | The token is stored in your remote configuration and verified | |
| 217 | - | against /admin/status on a best-effort basis. | |
| 216 | + | With --token: stores the token and verifies it. | |
| 217 | + | Without --token: verifies the existing stored token still works. | |
| 218 | 218 | ||
| 219 | 219 | Generate a token on the server with: gordon auth token generate | |
| 220 | 220 | ||
| 221 | 221 | Examples: | |
| 222 | 222 | gordon auth login --token <token> Store token for active remote | |
| 223 | - | gordon auth login --remote prod --token <tok> Store token for specific remote`, | |
| 223 | + | gordon auth login --remote prod --token <tok> Store token for specific remote | |
| 224 | + | gordon auth login Verify existing token`, | |
| 224 | 225 | RunE: func(cmd *cobra.Command, args []string) error { | |
| 225 | - | if token == "" { | |
| 226 | - | return fmt.Errorf("--token is required; generate a token on the server with: gordon auth token generate") | |
| 226 | + | if token != "" { | |
| 227 | + | return runAuthLoginWithToken(cmd.Context(), remoteName, token, cmd.OutOrStdout()) | |
| 227 | 228 | } | |
| 228 | - | return runAuthLoginWithToken(cmd.Context(), remoteName, token, cmd.OutOrStdout()) | |
| 229 | + | return runAuthLoginVerify(cmd.Context(), remoteName, cmd.OutOrStdout()) | |
| 229 | 230 | }, | |
| 230 | 231 | } | |
| 231 | 232 | ||
| @@ -268,6 +269,31 @@ func runAuthLoginWithToken(ctx context.Context, remoteName, token string, out io | |||
| 268 | 269 | return nil | |
| 269 | 270 | } | |
| 270 | 271 | ||
| 272 | + | func runAuthLoginVerify(ctx context.Context, remoteName string, out io.Writer) error { | |
| 273 | + | resolvedName, entry, err := resolveRemoteEntry(remoteName) | |
| 274 | + | if err != nil { | |
| 275 | + | return err | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | token := remote.ResolveTokenForRemote(resolvedName, entry) | |
| 279 | + | if token == "" { | |
| 280 | + | return fmt.Errorf("no token stored for remote '%s'; use: gordon auth login --token <token>", resolvedName) | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | insecureTLS := remote.ResolveInsecureTLSForRemote(insecureTLSFlag, resolvedName) | |
| 284 | + | client := remote.NewClient(entry.URL, remoteClientOptions(token, insecureTLS)...) | |
| 285 | + | verifyCtx, cancel := context.WithTimeout(ctx, 10*time.Second) | |
| 286 | + | defer cancel() | |
| 287 | + | ||
| 288 | + | if _, err := client.GetStatus(verifyCtx); err != nil { | |
| 289 | + | _ = cliWriteLine(out, styles.RenderWarning(fmt.Sprintf("Token verification failed for '%s': %v", resolvedName, err))) | |
| 290 | + | return fmt.Errorf("authentication failed") | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | _ = cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Authenticated to '%s'", resolvedName))) | |
| 294 | + | return nil | |
| 295 | + | } | |
| 296 | + | ||
| 271 | 297 | // newAuthShowTokenCmd creates the show-token command. | |
| 272 | 298 | func newAuthShowTokenCmd() *cobra.Command { | |
| 273 | 299 | var remoteName string | |
| @@ -211,21 +211,22 @@ func newAuthLoginCmd() *cobra.Command { | |||||
| 211 | cmd := &cobra.Command{ | 211 | cmd := &cobra.Command{ | ||
| 212 | Use: "login", | 212 | Use: "login", | ||
| 213 | Short: "Authenticate with a Gordon server", | 213 | Short: "Authenticate with a Gordon server", | ||
| 214 | - | Long: `Store a pre-generated token for a Gordon server remote. | 214 | + | Long: `Store or verify a token for a Gordon server remote. |
| 215 | 215 | ||||
| 216 | - | The token is stored in your remote configuration and verified | 216 | + | With --token: stores the token and verifies it. |
| 217 | - | against /admin/status on a best-effort basis. | 217 | + | Without --token: verifies the existing stored token still works. |
| 218 | 218 | ||||
| 219 | Generate a token on the server with: gordon auth token generate | 219 | Generate a token on the server with: gordon auth token generate | ||
| 220 | 220 | ||||
| 221 | Examples: | 221 | Examples: | ||
| 222 | gordon auth login --token <token> Store token for active remote | 222 | gordon auth login --token <token> Store token for active remote | ||
| 223 | - | gordon auth login --remote prod --token <tok> Store token for specific remote`, | 223 | + | gordon auth login --remote prod --token <tok> Store token for specific remote |
| 224 | + | gordon auth login Verify existing token`, | |||
| 224 | RunE: func(cmd *cobra.Command, args []string) error { | 225 | RunE: func(cmd *cobra.Command, args []string) error { | ||
| 225 | - | if token == "" { | 226 | + | if token != "" { |
| 226 | - | return fmt.Errorf("--token is required; generate a token on the server with: gordon auth token generate") | 227 | + | return runAuthLoginWithToken(cmd.Context(), remoteName, token, cmd.OutOrStdout()) |
| 227 | } | 228 | } | ||
| 228 | - | return runAuthLoginWithToken(cmd.Context(), remoteName, token, cmd.OutOrStdout()) | 229 | + | return runAuthLoginVerify(cmd.Context(), remoteName, cmd.OutOrStdout()) |
| 229 | }, | 230 | }, | ||
| 230 | } | 231 | } | ||
| 231 | 232 | ||||
| @@ -268,6 +269,31 @@ func runAuthLoginWithToken(ctx context.Context, remoteName, token string, out io | |||||
| 268 | return nil | 269 | return nil | ||
| 269 | } | 270 | } | ||
| 270 | 271 | ||||
| 272 | + | func runAuthLoginVerify(ctx context.Context, remoteName string, out io.Writer) error { | |||
| 273 | + | resolvedName, entry, err := resolveRemoteEntry(remoteName) | |||
| 274 | + | if err != nil { | |||
| 275 | + | return err | |||
| 276 | + | } | |||
| 277 | + | ||||
| 278 | + | token := remote.ResolveTokenForRemote(resolvedName, entry) | |||
| 279 | + | if token == "" { | |||
| 280 | + | return fmt.Errorf("no token stored for remote '%s'; use: gordon auth login --token <token>", resolvedName) | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | insecureTLS := remote.ResolveInsecureTLSForRemote(insecureTLSFlag, resolvedName) | |||
| 284 | + | client := remote.NewClient(entry.URL, remoteClientOptions(token, insecureTLS)...) | |||
| 285 | + | verifyCtx, cancel := context.WithTimeout(ctx, 10*time.Second) | |||
| 286 | + | defer cancel() | |||
| 287 | + | ||||
| 288 | + | if _, err := client.GetStatus(verifyCtx); err != nil { | |||
| 289 | + | _ = cliWriteLine(out, styles.RenderWarning(fmt.Sprintf("Token verification failed for '%s': %v", resolvedName, err))) | |||
| 290 | + | return fmt.Errorf("authentication failed") | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | _ = cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Authenticated to '%s'", resolvedName))) | |||
| 294 | + | return nil | |||
| 295 | + | } | |||
| 296 | + | ||||
| 271 | // newAuthShowTokenCmd creates the show-token command. | 297 | // newAuthShowTokenCmd creates the show-token command. | ||
| 272 | func newAuthShowTokenCmd() *cobra.Command { | 298 | func newAuthShowTokenCmd() *cobra.Command { | ||
| 273 | var remoteName string | 299 | var remoteName string | ||