Skip to content
2 changes: 1 addition & 1 deletion cmd/auth-clientCredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func auth_clientCredentials(cmd *cobra.Command, args []string) {
// Save the client credentials
p.Print("Storing client ID and secret in keyring... ")
if err := cp.Save(); err != nil {
fmt.Println("failed")
p.Println("failed")
cli.ExitWithError("An error occurred while storing client credentials", err)
}
p.Println("ok")
Expand Down
53 changes: 0 additions & 53 deletions cmd/auth-code.go

This file was deleted.

60 changes: 60 additions & 0 deletions cmd/auth-login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cmd

import (
"github.com/opentdf/otdfctl/pkg/auth"
"github.com/opentdf/otdfctl/pkg/cli"
"github.com/opentdf/otdfctl/pkg/man"
"github.com/opentdf/otdfctl/pkg/profiles"
"github.com/spf13/cobra"
)

func auth_codeLogin(cmd *cobra.Command, args []string) {
fh := cli.NewFlagHelper(cmd)
clientID := fh.GetOptionalString("client-id")
tlsNoVerify := fh.GetOptionalBool("tls-no-verify")

cp := InitProfile(cmd, false)
printer := cli.NewPrinter(true)

printer.Println("Initiating login...")
tok, publicClientID, err := auth.LoginWithPKCE(cp.GetEndpoint(), clientID, tlsNoVerify)
if err != nil {
cli.ExitWithError("could not authenticate", err)
}
printer.Println("ok")

// Set the auth credentials to profile
if err := cp.SetAuthCredentials(profiles.AuthCredentials{
AuthType: profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN,
AccessToken: profiles.AuthCredentialsAccessToken{
PublicClientID: publicClientID,
AccessToken: tok.AccessToken,
Expiration: tok.Expiry.Unix(),
RefreshToken: tok.RefreshToken,
},
}); err != nil {
cli.ExitWithError("failed to set auth credentials", err)
}

printer.Println("Storing credentials to profile in keyring...")
if err := cp.Save(); err != nil {
printer.Println("failed")
cli.ExitWithError("An error occurred while storing authentication credentials", err)
}
printer.Println("ok")
}

var codeLoginCmd *man.Doc

func init() {
codeLoginCmd = man.Docs.GetCommand("auth/login",
man.WithRun(auth_codeLogin),
)
codeLoginCmd.Flags().StringP(
codeLoginCmd.GetDocFlag("client-id").Name,
codeLoginCmd.GetDocFlag("client-id").Shorthand,
codeLoginCmd.GetDocFlag("client-id").Default,
codeLoginCmd.GetDocFlag("client-id").Description,
)
authCmd.AddCommand(&codeLoginCmd.Command)
}
42 changes: 42 additions & 0 deletions cmd/auth-logout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cmd

import (
"github.com/opentdf/otdfctl/pkg/auth"
"github.com/opentdf/otdfctl/pkg/cli"
"github.com/opentdf/otdfctl/pkg/man"
"github.com/opentdf/otdfctl/pkg/profiles"
"github.com/spf13/cobra"
)

func auth_logout(cmd *cobra.Command, args []string) {
fh := cli.NewFlagHelper(cmd)
tlsNoVerify := fh.GetOptionalBool("tls-no-verify")
cp := InitProfile(cmd, false)
printer := cli.NewPrinter(true)
printer.Println("Initiating logout...")

// we can only revoke access tokens stored for the code login flow, not client credentials
creds := cp.GetAuthCredentials()
if creds.AuthType == profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN {
printer.Println("Revoking access token...")
if err := auth.RevokeAccessToken(cp.GetEndpoint(), creds.AccessToken.PublicClientID, creds.AccessToken.RefreshToken, tlsNoVerify); err != nil {
printer.Println("failed")
cli.ExitWithError("An error occurred while revoking the access token", err)
}
}

if err := cp.SetAuthCredentials(profiles.AuthCredentials{}); err != nil {
printer.Println("failed")
cli.ExitWithError("An error occurred while logging out", err)
}
printer.Println("ok")
}

var codeLogoutCmd *man.Doc

func init() {
codeLogoutCmd = man.Docs.GetCommand("auth/logout",
man.WithRun(auth_logout),
)
authCmd.AddCommand(&codeLogoutCmd.Command)
}
15 changes: 7 additions & 8 deletions cmd/auth-printAccessToken.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/opentdf/otdfctl/pkg/man"
"github.com/opentdf/otdfctl/pkg/profiles"
"github.com/spf13/cobra"
"golang.org/x/oauth2"
)

var auth_printAccessTokenCmd = man.Docs.GetCommand("auth/print-access-token",
Expand All @@ -24,20 +23,20 @@ func auth_printAccessToken(cmd *cobra.Command, args []string) {
printEnabled := !jsonOut
p := cli.NewPrinter(printEnabled)

var tok *oauth2.Token
ac := cp.GetAuthCredentials()
switch ac.AuthType {
case profiles.PROFILE_AUTH_TYPE_CLIENT_CREDENTIALS:
var err error
p.Printf("Getting access token for %s... ", ac.ClientId)
tok, err = auth.GetTokenWithProfile(cmd.Context(), cp)
if err != nil {
p.Println("failed")
cli.ExitWithError("Failed to get token", err)
}
case profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN:
p.Printf("Getting profile's stored access token... ")
default:
cli.ExitWithError("Invalid auth type", nil)
}
tok, err := auth.GetTokenWithProfile(cmd.Context(), cp)
if err != nil {
p.Println("failed")
cli.ExitWithError("Failed to get token", err)
}
p.Println("ok")
p.Printf("Access Token: %s\n", tok.AccessToken)

Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func InitProfile(cmd *cobra.Command, onlyNew bool) *profiles.ProfileStore {
if profile.GetGlobalConfig().GetDefaultProfile() == "" {
cli.ExitWithWarning("No default profile set. Use `" + config.AppName + " profile create <profile> <endpoint>` to create a default profile.")
}
fmt.Printf("Using profile %s\n", profile.GetGlobalConfig().GetDefaultProfile())
fmt.Printf("Using profile [%s]\n", profile.GetGlobalConfig().GetDefaultProfile())

if profileName == "" {
profileName = profile.GetGlobalConfig().GetDefaultProfile()
Expand Down
9 changes: 3 additions & 6 deletions docs/man/auth/code-login.md → docs/man/auth/login.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
---
title: Open a browser and login with Auth Code PKCE
title: Open a browser and login

command:
name: code-login
name: login
flags:
- name: client-id
description: A clientId for use in auth code flow (default = platform well-known public_client_id)
shorthand: i
required: false
- name: no-cache
description: Do not cache credentials on the native OS (print access token to stdout)
default: false
---

Authenticate for use of the OpenTDF Platform through a browser (required).

Provide a specific public 'client-id' known to support the Auth Code PKCE flow and recognized
by the OpenTDF Platform, or use the default public client in the platform well-known configuration if not specified.

The OIDC Access Token will be stored in the OS-specific keychain by default, otherwise printed to `stdout` if `--no-cache` is passed.
The OIDC Access Token will be stored in the OS-specific keychain by default (Linux not yet supported).
9 changes: 9 additions & 0 deletions docs/man/auth/logout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Clear credentials from profile

command:
name: logout
---

Removes any auth credentials (Client Credentials or an Access Token from a login)
from the current profile.
71 changes: 52 additions & 19 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func GetClientCreds(endpoint string, file string, credsJSON []byte) (ClientCrede
return ClientCredentials{}, errors.New("no client credentials provided")
}

func getPlatformConfiguration(endpoint string, tlsNoVerify bool) (platformConfiguration, error) {
func getPlatformConfiguration(endpoint, publicClientID string, tlsNoVerify bool) (platformConfiguration, error) {
c := platformConfiguration{}

e, err := utils.NormalizeEndpoint(endpoint)
Expand Down Expand Up @@ -104,7 +104,6 @@ func getPlatformConfiguration(endpoint string, tlsNoVerify bool) (platformConfig

c.authzEndpoint, err = s.PlatformConfiguration.AuthzEndpoint()
if err != nil {

errs = append(errs, errors.Join(err, sdk.ErrPlatformAuthzEndpointNotFound))
}

Expand All @@ -113,9 +112,12 @@ func getPlatformConfiguration(endpoint string, tlsNoVerify bool) (platformConfig
errs = append(errs, errors.Join(err, sdk.ErrPlatformTokenEndpointNotFound))
}

c.publicClientID, err = s.PlatformConfiguration.PublicClientID()
if err != nil {
errs = append(errs, errors.Join(err, sdk.ErrPlatformPublicClientIDNotFound))
c.publicClientID = publicClientID
if c.publicClientID == "" {
c.publicClientID, err = s.PlatformConfiguration.PublicClientID()
if err != nil {
errs = append(errs, errors.Join(err, sdk.ErrPlatformPublicClientIDNotFound))
}
}

if len(errs) > 0 {
Expand All @@ -126,15 +128,23 @@ func getPlatformConfiguration(endpoint string, tlsNoVerify bool) (platformConfig
return c, nil
}

// func GetAccessTokenFromProfile() {}
func buildToken(c *profiles.AuthCredentials) *oauth2.Token {
return &oauth2.Token{
AccessToken: c.AccessToken.AccessToken,
Expiry: time.Unix(c.AccessToken.Expiration, 0),
RefreshToken: c.AccessToken.RefreshToken,
}
}

func GetSDKAuthOptionFromProfile(profile *profiles.ProfileStore) (sdk.Option, error) {
c := profile.GetAuthCredentials()

switch c.AuthType {
case profiles.PROFILE_AUTH_TYPE_CLIENT_CREDENTIALS:
return sdk.WithClientCredentials(c.ClientId, c.ClientSecret, nil), nil
// case profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN:
// return sdk.WithOAuthAccessTokenSource(o.authClientCredentials.AccessToken.AccessToken), nil
case profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN:
tokenSource := oauth2.StaticTokenSource(buildToken(&c))
return sdk.WithOAuthAccessTokenSource(tokenSource), nil
default:
return nil, ErrInvalidAuthType
}
Expand All @@ -151,28 +161,31 @@ func ValidateProfileAuthCredentials(ctx context.Context, profile *profiles.Profi
return err
}
return nil
// case profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN:
// return sdk.WithOAuthAccessTokenSource(o.authClientCredentials.AccessToken.AccessToken), nil
case profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN:
if !buildToken(&c).Valid() {
return ErrAccessTokenExpired
}
default:
return ErrInvalidAuthType
}
return nil
}

func GetTokenWithProfile(ctx context.Context, profile *profiles.ProfileStore) (*oauth2.Token, error) {
c := profile.GetAuthCredentials()
switch c.AuthType {
case profiles.PROFILE_AUTH_TYPE_CLIENT_CREDENTIALS:
return GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientId, c.ClientSecret, profile.GetTLSNoVerify())
// case profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN:
// return sdk.WithOAuthAccessTokenSource(o.authClientCredentials.AccessToken.AccessToken), nil
case profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN:
return buildToken(&c), nil
default:
return nil, ErrInvalidAuthType
}
}

// Uses the OAuth2 client credentials flow to obtain a token.
func GetTokenWithClientCreds(ctx context.Context, endpoint string, clientId string, clientSecret string, tlsNoVerify bool) (*oauth2.Token, error) {
pc, err := getPlatformConfiguration(endpoint, tlsNoVerify)
pc, err := getPlatformConfiguration(endpoint, "", tlsNoVerify)
if err != nil && !errors.Is(err, sdk.ErrPlatformPublicClientIDNotFound) {
return nil, err
}
Expand Down Expand Up @@ -230,20 +243,40 @@ func Login(platformEndpoint, tokenURL, authURL, publicClientID string) (*oauth2.
return uuid.New().String()
}
tok := oidcCLI.CodeFlow[*oidc.IDTokenClaims](ctx, relyingParty, authCallbackPath, authCodeFlowPort, stateProvider)
return tok.Token, nil
return &oauth2.Token{
AccessToken: tok.Token.AccessToken,
TokenType: tok.Token.TokenType,
RefreshToken: tok.Token.RefreshToken,
Expiry: tok.Token.Expiry,
}, nil
}

// Logs in using the auth code PKCE flow driven by the platform well-known idP OIDC configuration.
func LoginWithPKCE(host, publicClientID string, tlsNoVerify bool) (*oauth2.Token, error) {
pc, err := getPlatformConfiguration(host, tlsNoVerify)
func LoginWithPKCE(host, publicClientID string, tlsNoVerify bool) (*oauth2.Token, string, error) {
pc, err := getPlatformConfiguration(host, publicClientID, tlsNoVerify)
if err != nil {
return nil, fmt.Errorf("failed to get platform configuration: %w", err)
return nil, "", fmt.Errorf("failed to get platform configuration: %w", err)
}

tok, err := Login(host, pc.tokenEndpoint, pc.authzEndpoint, pc.publicClientID)
if err != nil {
return nil, fmt.Errorf("failed to login: %w", err)
return nil, "", fmt.Errorf("failed to login: %w", err)
}

return tok, pc.publicClientID, nil
}

// Revokes the access token
func RevokeAccessToken(endpoint, publicClientID, refreshToken string, tlsNoVerify bool) error {
pCfg, err := getPlatformConfiguration(endpoint, publicClientID, tlsNoVerify)
if err != nil {
return fmt.Errorf("failed to get platform configuration: %w", err)
}

rp, err := oidcrp.NewRelyingPartyOIDC(context.Background(), pCfg.issuer, pCfg.publicClientID, "", "", nil)
if err != nil {
return err
}

return tok, nil
return oidcrp.RevokeToken(context.Background(), rp, refreshToken, "refresh_token")
}
Loading