Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions cmd/tokitoki/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ func run(args []string) int {
if len(args) > 0 && args[0] == "get" {
return runGet(args[1:])
}
if len(args) > 0 && args[0] == "verify" {
return runVerify(args[1:])
}
if len(args) > 0 && args[0] == "__service-run" {
return runServiceWorker(args[1:])
}
Expand Down Expand Up @@ -272,6 +275,35 @@ func runGet(args []string) int {
return 0
}

func runVerify(args []string) int {
if len(args) != 1 || args[0] != "key" {
fmt.Fprintln(os.Stderr, "usage: tokitoki verify key")
return 2
}

logger := defaultLogger()
client, err := agentlib.New(agentlib.Options{Logger: logger})
if err != nil {
return fail(logger, err)
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

// An invalid key is a definite answer, not a failure: exit 0 with
// valid:false so callers can tell it apart from "could not check".
valid, err := client.VerifyAPIKey(ctx)
if err != nil {
return fail(logger, err)
}
if err := writeJSON(os.Stdout, map[string]any{"ok": true, "valid": valid}); err != nil {
return fail(logger, err)
}
return 0
}

func runUpdate(args []string) int {
if len(args) != 0 {
fmt.Fprintln(os.Stderr, "usage: tokitoki update")
Expand Down Expand Up @@ -522,6 +554,7 @@ Usage:
tokitoki set key <API_KEY>
tokitoki get key
tokitoki get dashboard-url
tokitoki verify key
tokitoki heartbeat --entity FILE [options]
tokitoki version
tokitoki update
Expand Down
34 changes: 34 additions & 0 deletions internal/deviceauth/deviceauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,37 @@ func DashboardURL(ctx context.Context, baseURL, apiKey string) (string, error) {
}
return decoded.URL, nil
}

// VerifyKey asks the server whether apiKey is currently valid. A definite
// server answer returns (true, nil) or (false, nil); anything else — network
// failure, server error — is an error, so callers can tell "invalid key"
// apart from "could not check".
func VerifyKey(ctx context.Context, baseURL, apiKey string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/api/auth/api-key/verify", nil)
if err != nil {
return false, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("User-Agent", buildinfo.UserAgent())

resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, fmt.Errorf("verify API key: %w", err)
}
defer resp.Body.Close()

switch resp.StatusCode {
case http.StatusOK:
var decoded struct {
Valid bool `json:"valid"`
}
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
return false, fmt.Errorf("verify API key: %w", err)
}
return decoded.Valid, nil
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("verify API key: server returned %s", resp.Status)
}
}
14 changes: 14 additions & 0 deletions pkg/agentlib/agentlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,20 @@ func (c *Client) DashboardURL(ctx context.Context) (string, error) {
return deviceauth.DashboardURL(ctx, usageupload.BaseURL(), apiKey)
}

// VerifyAPIKey checks the stored API key against the server. A definite
// answer returns (true, nil) or (false, nil); network or server trouble is
// an error so callers can tell "invalid" apart from "could not check".
func (c *Client) VerifyAPIKey(ctx context.Context) (bool, error) {
apiKey, err := c.GetAPIKey()
if err != nil {
return false, err
}
if ctx == nil {
ctx = context.Background()
}
return deviceauth.VerifyKey(ctx, usageupload.BaseURL(), apiKey)
}

// Sync scans selected provider directories and uploads newly discovered events.
func (c *Client) Sync(ctx context.Context, options SyncOptions) error {
providerDirs := normalizeProviderDirs(options.ProviderDirs)
Expand Down