feat(tpagectl): implement authentication methods #585

Merged
fuzzy merged 2 commits from feat/cli-auth into main 2026-08-07 14:37:16 +00:00
Owner

What

Implements Phase 6 authentication methods.

#250 / #251 (already implemented, now documented as complete)

  • The client sends Authorization: Bearer <token> from config.server.token (client.go), covering API key/token from config (#250) and Authentik token support (#251 — the Authentik JWT passes through as the bearer and the daemon verifies it).

#252 — interactive device-flow login command (new)

  • internal/tpagectl/auth package with a top-level login command registered on the root.
  • Config gains auth.issuer and auth.client_id (mapstructure:"auth"); credentials resolve from --issuer/--client-id flags or config.
  • OIDC discovery ({issuer}/.well-known/openid-configuration) supplies the device authorization and token endpoints; golang.org/x/oauth2 (promoted to a direct dependency) performs the device flow (DeviceAuth + DeviceAccessToken polling with a --timeout).
  • On success the token is stored via new config.SetToken write support into the server.token field of the config file (the file Load would read, or the default $XDG_CONFIG_HOME/tpagectl/.tpagectl.yaml when none exists), so subsequent commands authenticate automatically.
  • config gains WithContext/FromContext context helpers (mirroring the client), used to fall back to config credentials.

Why

Phase 6 roadmap task #253.

Testing

  • Config: SetToken writes to an existing file (preserving other fields) and creates the default file; auth.* env mapping; context helpers
  • Auth: device-flow happy path (discovery + device + form-encoded token endpoint → token stored in config), missing issuer/client-id, discovery error
  • Wiring: root help lists login, login --help renders
  • go test -race ./... passes (23 packages)
  • golangci-lint run clean
  • pre-commit hooks pass (gofmt, go mod tidy, go test)

Breaking Changes

None. golang.org/x/oauth2 promoted from indirect to a direct dependency.

Notes

Token storage here is the interim config-file approach per decision; OS keychain credential management is a separate roadmap task (#257).

Closes #250
Closes #251
Closes #252
Closes #253

## What Implements Phase 6 authentication methods. **#250 / #251 (already implemented, now documented as complete)** - The client sends `Authorization: Bearer <token>` from `config.server.token` (client.go), covering API key/token from config (#250) and Authentik token support (#251 — the Authentik JWT passes through as the bearer and the daemon verifies it). **#252 — interactive device-flow `login` command (new)** - `internal/tpagectl/auth` package with a top-level `login` command registered on the root. - Config gains `auth.issuer` and `auth.client_id` (`mapstructure:"auth"`); credentials resolve from `--issuer`/`--client-id` flags or config. - OIDC discovery (`{issuer}/.well-known/openid-configuration`) supplies the device authorization and token endpoints; `golang.org/x/oauth2` (promoted to a direct dependency) performs the device flow (`DeviceAuth` + `DeviceAccessToken` polling with a `--timeout`). - On success the token is stored via new `config.SetToken` write support into the `server.token` field of the config file (the file `Load` would read, or the default `$XDG_CONFIG_HOME/tpagectl/.tpagectl.yaml` when none exists), so subsequent commands authenticate automatically. - `config` gains `WithContext`/`FromContext` context helpers (mirroring the client), used to fall back to config credentials. ## Why Phase 6 roadmap task #253. ## Testing - [x] Config: `SetToken` writes to an existing file (preserving other fields) and creates the default file; `auth.*` env mapping; context helpers - [x] Auth: device-flow happy path (discovery + device + form-encoded token endpoint → token stored in config), missing issuer/client-id, discovery error - [x] Wiring: root help lists `login`, `login --help` renders - [x] `go test -race ./...` passes (23 packages) - [x] `golangci-lint run` clean - [x] pre-commit hooks pass (gofmt, go mod tidy, go test) ## Breaking Changes None. `golang.org/x/oauth2` promoted from indirect to a direct dependency. ## Notes Token storage here is the interim config-file approach per decision; OS keychain credential management is a separate roadmap task (#257). Closes #250 Closes #251 Closes #252 Closes #253
Add the login command, which authenticates to the thwap-pagesd daemon
through Authentik using the OAuth2 device flow (golang.org/x/oauth2
promoted to a direct dependency). The issuer and client ID come from
--issuer/--client-id or the new auth.issuer/auth.client_id config fields;
the OIDC discovery document provides the device and token endpoints. The
obtained token is written to the server.token field of the config file via
new config.SetToken write support, so subsequent commands authenticate
automatically.

Token-based bearer authentication (config server.token) already covered
API key and Authentik token support.
docs(roadmap): mark authentication methods complete
All checks were successful
Test and Release / lint (pull_request) Successful in 7m23s
Test and Release / test (pull_request) Successful in 7m33s
c358c9e49e
Record completion of the tpagectl authentication methods: bearer token
from config (API key and Authentik token) and the interactive device-flow
login command that stores the obtained token in the config file.

closes #250
closes #251
closes #252
closes #253
fuzzy scheduled this pull request to auto merge when all checks succeed 2026-08-07 14:30:09 +00:00
the.auditor left a comment

Summary

Phase 6 authentication: login command (OIDC device flow) + config.SetToken write support + auth.* config. Verified: go test -race ./... passes (23 packages) and golangci-lint run ./... is clean. Closes #250-253.

Verified: golang.org/x/oauth2 device flow used correctly (DeviceAuth + DeviceAccessToken), token stored via viper write; auth.issuer/auth.client_id resolve from flags then config; WithContext/FromContext for config mirrors the client.

No blocking issues.

Suggestions (filed as issues)

  1. internal/tpagectl/config/config.go (SetToken) – the token is written via viper WriteConfig, whose default configPermissions is 0o644 (confirmed in the module source), leaving the API token world-readable. Filed as #586 (Priority/High)
  2. internal/tpagectl/auth/login.go (discoverEndpoints) – uses http.DefaultClient with no timeout; a blackholed issuer hangs login indefinitely. Filed as #587

References / Notes

  • login runs the root Before hook and builds a daemon HTTP client it never uses — extends #582
  • configFromContext in main.go still swallows the FromContext error → nil — extends #549
  • SetToken writes to the first config path Load finds; if that is a system path (/etc/tpagectl), an unprivileged login would fail — consider always writing to the user config path
  • Plaintext interim storage is documented (#257 keychain deferred) — acceptable, but 0600 permissions are still warranted

Praise

  • Clean device-flow implementation via x/oauth2 with --timeout-bounded token polling
  • Tidy newViper refactor; WithContext/FromContext mirrors the client pattern
  • Good coverage: SetToken preserve/create, auth env mapping, device-flow happy path, missing credentials, discovery error
## Summary Phase 6 authentication: `login` command (OIDC device flow) + `config.SetToken` write support + `auth.*` config. Verified: `go test -race ./...` passes (23 packages) and `golangci-lint run ./...` is clean. Closes #250-253. Verified: `golang.org/x/oauth2` device flow used correctly (`DeviceAuth` + `DeviceAccessToken`), token stored via viper write; `auth.issuer`/`auth.client_id` resolve from flags then config; `WithContext`/`FromContext` for config mirrors the client. No blocking issues. ## Suggestions (filed as issues) 1. `internal/tpagectl/config/config.go` (`SetToken`) – the token is written via viper `WriteConfig`, whose default `configPermissions` is `0o644` (confirmed in the module source), leaving the API token world-readable. Filed as #586 (Priority/High) 2. `internal/tpagectl/auth/login.go` (`discoverEndpoints`) – uses `http.DefaultClient` with no timeout; a blackholed issuer hangs `login` indefinitely. Filed as #587 ## References / Notes - `login` runs the root `Before` hook and builds a daemon HTTP client it never uses — extends #582 - `configFromContext` in `main.go` still swallows the `FromContext` error → nil — extends #549 - `SetToken` writes to the first config path `Load` finds; if that is a system path (`/etc/tpagectl`), an unprivileged `login` would fail — consider always writing to the user config path - Plaintext interim storage is documented (#257 keychain deferred) — acceptable, but 0600 permissions are still warranted ## Praise - Clean device-flow implementation via x/oauth2 with `--timeout`-bounded token polling - Tidy `newViper` refactor; `WithContext`/`FromContext` mirrors the client pattern - Good coverage: SetToken preserve/create, auth env mapping, device-flow happy path, missing credentials, discovery error
fuzzy merged commit c358c9e49e into main 2026-08-07 14:37:16 +00:00
fuzzy deleted branch feat/cli-auth 2026-08-07 14:37:16 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
thwap/thwap-pagesd!585
No description provided.