Skip to content

Commit

Permalink
feat: improve auth with client credentials (#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrschumacher authored Aug 19, 2024
1 parent a7aa89f commit 9c4968f
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 234 deletions.
21 changes: 17 additions & 4 deletions cmd/auth-clearCachedCredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/spf13/cobra"
)

var clearCachedCredsCmd = man.Docs.GetCommand("auth/clear-cached-credentials",
var auth_clearClientCredentialsCmd = man.Docs.GetCommand("auth/clear-client-credentials",
man.WithRun(auth_clearCreds),
man.WithHiddenFlags("with-client-creds", "with-client-creds-file"),
)
Expand All @@ -18,9 +18,22 @@ func auth_clearCreds(cmd *cobra.Command, args []string) {
flagHelper := cli.NewFlagHelper(cmd)
host := flagHelper.GetRequiredString("host")

if err := handlers.ClearCachedCredentials(host); err != nil {
cli.ExitWithError("Failed to clear cached client credentials and token", err)
p := cli.NewPrinter(true)

p.Printf("Clearing cached client credentials for %s... ", host)
if err := handlers.NewKeyring(host).DeleteClientCredentials(); err != nil {
fmt.Println("failed")
cli.ExitWithError("Failed to clear cached client credentials", err)
}
p.Println("ok")
}

func init() {
auth_clearClientCredentialsCmd.Flags().String(
auth_clearClientCredentialsCmd.GetDocFlag("all").Name,
auth_clearClientCredentialsCmd.GetDocFlag("all").Description,
auth_clearClientCredentialsCmd.GetDocFlag("all").Default,
)

fmt.Println(cli.SuccessMessage("Cached client credentials and token are clear."))
authCmd.AddCommand(&auth_clearClientCredentialsCmd.Command)
}
89 changes: 26 additions & 63 deletions cmd/auth-clientCredentials.go
Original file line number Diff line number Diff line change
@@ -1,94 +1,57 @@
package cmd

import (
"errors"
"fmt"
"log/slog"

"github.com/opentdf/otdfctl/pkg/cli"
"github.com/opentdf/otdfctl/pkg/handlers"
"github.com/opentdf/otdfctl/pkg/man"
"github.com/spf13/cobra"
)

var (
clientCredentialsCmd = man.Docs.GetCommand("auth/client-credentials",
man.WithRun(auth_clientCredentials),
)
noCacheCreds bool
var clientCredentialsCmd = man.Docs.GetCommand("auth/client-credentials",
man.WithRun(auth_clientCredentials),
man.WithHiddenFlags("with-client-creds", "with-client-creds-file"),
)

func auth_clientCredentials(cmd *cobra.Command, args []string) {
var err error
var c handlers.ClientCredentials

flagHelper := cli.NewFlagHelper(cmd)
host := flagHelper.GetRequiredString("host")
tlsNoVerify := flagHelper.GetOptionalBool("tls-no-verify")
clientID := flagHelper.GetOptionalString("client-id")
clientSecret := flagHelper.GetOptionalString("client-secret")

slog.Debug("Checking for client credentials file", slog.String("with-client-creds-file", clientCredsFile))
if clientCredsFile != "" {
creds, err := handlers.GetClientCredsFromFile(clientCredsFile)
if err != nil {
cli.ExitWithError("Failed to parse client credentials JSON", err)
}
clientID = creds.ClientID
clientSecret = creds.ClientSecret
}
p := cli.NewPrinter(true)

// if not provided by flag, check keyring cache for clientID
if clientID == "" {
slog.Debug("No client-id provided. Attempting to retrieve the default from keyring.")
clientID, err = handlers.GetClientIDFromCache(host)
if err != nil || clientID == "" {
cli.ExitWithError("Please provide required flag: (client-id)", errors.New("no client-id found"))
} else {
slog.Debug(cli.SuccessMessage("Retrieved stored client-id from keyring"))
}
if len(args) > 0 {
c.ClientId = args[0]
}
if len(args) > 1 {
c.ClientSecret = args[1]
}

// check if we have a clientSecret in the keyring, if a null value is passed in
if clientSecret == "" {
clientSecret, err = handlers.GetClientSecretFromCache(host, clientID)
if err == nil || clientSecret == "" {
cli.ExitWithError("Please provide required flag: (client-secret)", errors.New("no client-secret found"))
} else {
slog.Debug("Retrieved stored client-secret from keyring")
}
if c.ClientId == "" {
c.ClientId = cli.AskForInput("Enter client id: ")
}
if c.ClientSecret == "" {
c.ClientSecret = cli.AskForSecret("Enter client secret: ")
}

slog.Debug("Attempting to login with client credentials", slog.String("client-id", clientID))
if err := handlers.GetTokenWithClientCreds(cmd.Context(), host, clientID, clientSecret, tlsNoVerify); err != nil {
p.Printf("Logging in with client ID and secret for %s... ", host)
if _, err := handlers.GetTokenWithClientCreds(cmd.Context(), host, c, tlsNoVerify); err != nil {
fmt.Println("failed")
cli.ExitWithError("An error occurred during login. Please check your credentials and try again", err)
}
p.Println("ok")

fmt.Println(cli.SuccessMessage("Successfully logged in with client ID and secret"))
p.Print("Storing client ID and secret in keyring... ")
if err := handlers.NewKeyring(host).SetClientCredentials(c); err != nil {
fmt.Println("failed")
cli.ExitWithError("Failed to cache client credentials", err)
}
p.Println("ok")
}

func init() {
clientCredentialsCmd := man.Docs.GetCommand("auth/client-credentials",
man.WithRun(auth_clientCredentials),
// use the individual client-id and client-secret flags here instead of the global with-client-creds flag
man.WithHiddenFlags("with-client-creds", "with-client-creds-file"),
)
clientCredentialsCmd.Flags().StringP(
clientCredentialsCmd.GetDocFlag("client-id").Name,
clientCredentialsCmd.GetDocFlag("client-id").Shorthand,
clientCredentialsCmd.GetDocFlag("client-id").Default,
clientCredentialsCmd.GetDocFlag("client-id").Description,
)
clientCredentialsCmd.Flags().StringP(
clientCredentialsCmd.GetDocFlag("client-secret").Name,
clientCredentialsCmd.GetDocFlag("client-secret").Shorthand,
clientCredentialsCmd.GetDocFlag("client-secret").Default,
clientCredentialsCmd.GetDocFlag("client-secret").Description,
)
clientCredentialsCmd.Flags().BoolVarP(
&noCacheCreds,
clientCredentialsCmd.GetDocFlag("no-cache").Name,
clientCredentialsCmd.GetDocFlag("no-cache").Shorthand,
clientCredentialsCmd.GetDocFlag("no-cache").DefaultAsBool(),
clientCredentialsCmd.GetDocFlag("no-cache").Description,
)
authCmd.AddCommand(&clientCredentialsCmd.Command)
}
53 changes: 47 additions & 6 deletions cmd/auth-printAccessToken.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"context"
"encoding/json"
"fmt"

"github.com/opentdf/otdfctl/pkg/cli"
Expand All @@ -9,17 +11,56 @@ import (
"github.com/spf13/cobra"
)

var printAccessToken = man.Docs.GetCommand("auth/print-access-token",
man.WithRun(auth_printAccessToken),
)
var auth_printAccessTokenCmd = man.Docs.GetCommand("auth/print-access-token",
man.WithRun(auth_printAccessToken))

func auth_printAccessToken(cmd *cobra.Command, args []string) {
flagHelper := cli.NewFlagHelper(cmd)
host := flagHelper.GetRequiredString("host")
jsonOut := flagHelper.GetOptionalBool("json")

printEnabled := !jsonOut
p := cli.NewPrinter(printEnabled)

p.Printf("Getting stored client credentials for %s... ", host)
clientCredentials, err := handlers.NewKeyring(host).GetClientCredentials()
if err != nil {
p.Println("failed")
cli.ExitWithError("Client credentials not found. Please use `auth client-credentials` to set them", err)
}
p.Println("ok")

tok, err := handlers.GetOIDCTokenFromCache(host)
p.Printf("Getting access token for %s... ", clientCredentials.ClientId)
tok, err := handlers.GetTokenWithClientCreds(
context.Background(),
host,
clientCredentials,
flagHelper.GetOptionalBool("tls-no-verify"),
)
if err != nil {
cli.ExitWithError("Failed to get OIDC token from cache", err)
p.Println("failed")
cli.ExitWithError("Failed to get token", err)
}
fmt.Print(tok)
p.Println("ok")
p.Printf("Access Token: %s\n", tok.AccessToken)

if jsonOut {
d, err := json.MarshalIndent(tok, "", " ")
if err != nil {
cli.ExitWithError("Failed to marshal token to json", err)
}

fmt.Println(string(d))
return
}
}

func init() {
auth_printAccessTokenCmd.Flags().Bool(
auth_printAccessTokenCmd.GetDocFlag("json").Name,
auth_printAccessTokenCmd.GetDocFlag("json").DefaultAsBool(),
auth_printAccessTokenCmd.GetDocFlag("json").Description,
)

authCmd.AddCommand(&auth_printAccessTokenCmd.Command)
}
15 changes: 7 additions & 8 deletions cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (
"github.com/spf13/cobra"
)

var authCmd = man.Docs.GetCommand("auth", man.WithHiddenFlags(
"with-client-creds",
"with-client-creds-file",
))

func init() {
cmd := man.Docs.GetCommand("auth",
man.WithSubcommands(clientCredentialsCmd),
man.WithSubcommands(printAccessToken),
man.WithSubcommands(clearCachedCredsCmd),
)
RootCmd.AddCommand(&authCmd.Command)

cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
authCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
// not supported on linux
if runtime.GOOS == "linux" {
cli.ExitWithWarning(
Expand All @@ -24,6 +25,4 @@ func init() {
)
}
}

RootCmd.AddCommand(&cmd.Command)
}
2 changes: 1 addition & 1 deletion cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func NewHandler(cmd *cobra.Command) handlers.Handler {
cli.ExitWithError("Failed to get client credentials", err)
}

h, err := handlers.NewWithCredentials(host, creds.ClientID, creds.ClientSecret, tlsNoVerify)
h, err := handlers.NewWithCredentials(host, creds.ClientId, creds.ClientSecret, tlsNoVerify)
if err != nil {
if errors.Is(err, handlers.ErrUnauthenticated) {
cli.ExitWithError(fmt.Sprintf("Not logged in. Please authenticate via CLI auth flow(s) before using command (%s %s)", cmd.Parent().Use, cmd.Use), err)
Expand Down
8 changes: 0 additions & 8 deletions docs/man/auth/clear-cached-credentials.md

This file was deleted.

12 changes: 12 additions & 0 deletions docs/man/auth/clear-client-credentials.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Clear the cached client credentials

command:
name: clear-client-credentials
flags:
- name: all
description: Clear all cached client credentials
default: false
---

Clear the cached client credentials from the OS keyring for the current platform endpoint.
18 changes: 6 additions & 12 deletions docs/man/auth/client-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@ title: Authenticate to the platform with the client-credentials flow

command:
name: client-credentials
flags:
- name: client-id
description: A clientId for use in client-credentials auth flow
shorthand: i
required: true
- name: client-secret
description: A clientSecret for use in client-credentials auth flow
shorthand: s
- name: no-cache
description: Do not cache credentials on the native OS and print access token to stdout instead
args:
- client-id
arbitrary_args:
- client-secret
---

Allows the user to login in via Client ID and Secret. The client credentials and OIDC Access Token will be stored
in the OS-specific keychain by default, otherwise printed to `stdout` if `--no-cache` is passed.
Allows the user to login in via Client Credentials flow. The client credentials will be stored safely
in the OS keyring for future use.
6 changes: 5 additions & 1 deletion docs/man/auth/print-access-token.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ title: Print the cached OIDC access token (if found)

command:
name: print-access-token
flags:
- name: json
description: Print the full token in JSON format
default: false
---

Retrieves the cached OIDC Access Token from the OS-specific keychain and prints to stdout if found.
Retrieves a new OIDC Access Token using the client credentials from the OS-specific keychain and prints to stdout if found.
13 changes: 13 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/zalando/go-keyring v0.2.5
golang.org/x/oauth2 v0.22.0
golang.org/x/term v0.22.0
google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.34.2
Expand All @@ -47,10 +48,14 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gowebpki/jcs v1.0.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
Expand All @@ -75,6 +80,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opentdf/platform/lib/ocrypto v0.1.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
Expand All @@ -85,6 +91,7 @@ require (
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
Expand All @@ -96,6 +103,12 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.5.4 // indirect
github.com/yuin/goldmark-emoji v1.0.2 // indirect
github.com/zitadel/logging v0.6.0 // indirect
github.com/zitadel/oidc/v3 v3.27.0 // indirect
github.com/zitadel/schema v1.3.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
Expand Down
Loading

0 comments on commit 9c4968f

Please sign in to comment.