Skip to content

Commit 8afd266

Browse files
oauth support
oauth support
1 parent bc01b35 commit 8afd266

35 files changed

Lines changed: 2029 additions & 115 deletions

README.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ The binary from `go install` is named `glassnode-cli`. To get the same `gn` comm
4343
## Quick Start
4444

4545
```bash
46-
# Set your API key
47-
export GLASSNODE_API_KEY=your-key
46+
# Sign in with your Glassnode account
47+
gn login
4848

4949
# List available assets
5050
gn asset list
@@ -53,15 +53,39 @@ gn asset list
5353
gn metric get market/price_usd_close --asset BTC --since 30d
5454
```
5555

56+
Or authenticate with an API key instead:
57+
58+
```bash
59+
export GLASSNODE_API_KEY=your-key
60+
```
61+
5662
## Authentication
5763

64+
### OAuth
65+
66+
Sign in with your Glassnode account:
67+
68+
```bash
69+
gn login
70+
```
71+
72+
And to sign out:
73+
74+
```bash
75+
gn logout
76+
```
77+
78+
### API key
79+
5880
The CLI resolves the API key in the following priority order:
5981

6082
1. `--api-key` flag (highest priority)
6183
2. `GLASSNODE_API_KEY` environment variable
6284
3. `~/.gn/config.yaml` configuration file
6385

64-
To persist the key in the config file:
86+
However the OAuth access token has precedence and will be used if it is available.
87+
88+
To persist the API key in the config file:
6589

6690
```bash
6791
gn config set api-key=your-key
@@ -193,13 +217,15 @@ gn config set output=csv
193217

194218
### `gn config get <key|all>`
195219

196-
Read a configuration value, or all values.
220+
Read a configuration value, or all values. Sensitive values (`api-key`, `oauth-access-token`, `oauth-refresh-token`) are **masked** as `*****-<last4>` so they're safe to show in screen shares, recordings, or shell scrollback.
197221

198222
```bash
199-
gn config get api-key
223+
gn config get api-key # e.g. "*****-cdef"
200224
gn config get all
201225
```
202226

227+
OAuth session fields (`oauth-access-token`, `oauth-refresh-token`, `oauth-expires-at`) are read-only: they are written only by `gn login` / token refresh and cannot be changed via `gn config set`.
228+
203229
## Global Flags
204230

205231
| Flag | Short | Description |

cmd/asset_describe.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ var assetDescribeCmd = &cobra.Command{
1818
escaped := strings.ReplaceAll(assetID, "'", "''")
1919

2020
apiKeyFlag, _ := cmd.Flags().GetString("api-key")
21-
apiKey, err := api.RequireAPIKey(apiKeyFlag)
21+
apiKey, bearer, err := api.RequireAuth(cmd.Context(), apiKeyFlag)
2222
if err != nil {
2323
return err
2424
}
25-
client := api.NewClient(apiKey)
25+
client := api.NewClient(apiKey, bearer)
2626

2727
filter := fmt.Sprintf("asset.id=='%s'", escaped)
2828

cmd/asset_list.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ var assetListCmd = &cobra.Command{
3131
Short: "List available assets",
3232
RunE: func(cmd *cobra.Command, args []string) error {
3333
apiKeyFlag, _ := cmd.Flags().GetString("api-key")
34-
apiKey, err := api.RequireAPIKey(apiKeyFlag)
34+
apiKey, bearer, err := api.RequireAuth(cmd.Context(), apiKeyFlag)
3535
if err != nil {
3636
return err
3737
}
38-
client := api.NewClient(apiKey)
38+
client := api.NewClient(apiKey, bearer)
3939

4040
filter, _ := cmd.Flags().GetString("filter")
4141
filter = normalizeFilter(filter)

cmd/asset_list_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"testing"
1313
)
1414

15-
func runCLIAssetList(t *testing.T, baseURL string, args ...string) (stdout, stderr string, err error) {
15+
func runCLIAssetList(t *testing.T, baseURL string, args ...string) (string, string, error) {
1616
t.Helper()
1717
_, f, _, _ := runtime.Caller(0)
1818
root := filepath.Join(filepath.Dir(f), "..")

cmd/config_get.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
var configGetCmd = &cobra.Command{
1111
Use: "get <key>",
12-
Short: "Get a configuration value (use 'all' to show all)",
12+
Short: "Get a configuration value (use 'all' to show all). Sensitive values are masked.",
1313
Args: cobra.ExactArgs(1),
1414
RunE: func(cmd *cobra.Command, args []string) error {
1515
if args[0] == "all" {
@@ -18,8 +18,7 @@ var configGetCmd = &cobra.Command{
1818
return err
1919
}
2020
for k, v := range values {
21-
_, err := fmt.Fprintf(cmd.OutOrStdout(), "%s=%s\n", k, v)
22-
if err != nil {
21+
if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s=%s\n", k, config.Mask(k, v)); err != nil {
2322
return err
2423
}
2524
}
@@ -30,10 +29,7 @@ var configGetCmd = &cobra.Command{
3029
if err != nil {
3130
return err
3231
}
33-
_, err = fmt.Fprintln(cmd.OutOrStdout(), value)
34-
if err != nil {
35-
return err
36-
}
37-
return nil
32+
_, err = fmt.Fprintln(cmd.OutOrStdout(), config.Mask(args[0], value))
33+
return err
3834
},
3935
}

cmd/credits.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ var creditsCmd = &cobra.Command{
1313
Short: "Show the API credits summary for your account",
1414
RunE: func(cmd *cobra.Command, args []string) error {
1515
apiKeyFlag, _ := cmd.Flags().GetString("api-key")
16-
apiKey, err := api.RequireAPIKey(apiKeyFlag)
16+
apiKey, bearer, err := api.RequireAuth(cmd.Context(), apiKeyFlag)
1717
if err != nil {
1818
return err
1919
}
2020

21-
client := api.NewClient(apiKey)
21+
client := api.NewClient(apiKey, bearer)
2222

2323
dryRun, _ := cmd.Flags().GetBool("dry-run")
2424
if dryRun {

cmd/credits_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
)
77

88
func TestCredits_DryRun_RedactsKey(t *testing.T) {
9-
stdout, stderr, err := runCLI(t, nil,
9+
stdout, stderr, err := runCLI(t, []string{"HOME=" + t.TempDir()},
1010
"user", "credits", "--api-key", "secret-key", "--dry-run",
1111
)
1212
if err != nil {

cmd/login.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/glassnode/glassnode-cli/internal/oauth"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var loginCmd = &cobra.Command{
11+
Use: "login",
12+
Short: "Sign in with your Glassnode account in the browser (OAuth 2.0 with PKCE)",
13+
Long: `Opens your browser to complete sign-in. Tokens are stored in ~/.gn/config.yaml.`,
14+
RunE: runLogin,
15+
}
16+
17+
func runLogin(cmd *cobra.Command, _ []string) error {
18+
if err := oauth.RunLogin(cmd.Context()); err != nil {
19+
return err
20+
}
21+
_, err := fmt.Fprintln(cmd.OutOrStdout(), "Signed in successfully.")
22+
return err
23+
}
24+
25+
func init() {
26+
rootCmd.AddCommand(loginCmd)
27+
}

cmd/logout.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/glassnode/glassnode-cli/internal/oauth"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var logoutCmd = &cobra.Command{
12+
Use: "logout",
13+
Short: "Revoke the stored OAuth tokens and clear them from ~/.gn/config.yaml",
14+
Long: `Revokes the OAuth refresh and access tokens. Local tokens are always cleared even if the remote revocation failed.`,
15+
RunE: runLogout,
16+
}
17+
18+
func runLogout(cmd *cobra.Command, _ []string) error {
19+
err := oauth.Logout(cmd.Context())
20+
var remoteErr *oauth.RemoteRevokeError
21+
if errors.As(err, &remoteErr) {
22+
_, _ = fmt.Fprintf(cmd.ErrOrStderr(),
23+
"warning: remote token revocation failed but local tokens were removed: %v\n",
24+
remoteErr)
25+
} else if err != nil {
26+
return err
27+
}
28+
_, err = fmt.Fprintln(cmd.OutOrStdout(), "Signed out.")
29+
return err
30+
}
31+
32+
func init() {
33+
rootCmd.AddCommand(logoutCmd)
34+
}

cmd/metric_describe.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ var metricDescribeCmd = &cobra.Command{
1616
path := api.NormalizePath(args[0])
1717

1818
apiKeyFlag, _ := cmd.Flags().GetString("api-key")
19-
apiKey, err := api.RequireAPIKey(apiKeyFlag)
19+
apiKey, bearer, err := api.RequireAuth(cmd.Context(), apiKeyFlag)
2020
if err != nil {
2121
return err
2222
}
23-
client := api.NewClient(apiKey)
23+
client := api.NewClient(apiKey, bearer)
2424

2525
asset, _ := cmd.Flags().GetString("asset")
2626

0 commit comments

Comments
 (0)