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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ otdfctl.yaml

# Ignore the tructl binary
otdfctl
otdfctl_testbuild

# Misc
creds.json

# Hugo
public/
Expand Down
8 changes: 4 additions & 4 deletions cmd/auth-login.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func auth_codeLogin(cmd *cobra.Command, args []string) {
printer := cli.NewPrinter(true)

printer.Println("Initiating login...")
tok, publicClientID, err := auth.LoginWithPKCE(cp.GetEndpoint(), clientID, tlsNoVerify)
tok, publicClientID, err := auth.LoginWithPKCE(cmd.Context(), cp.GetEndpoint(), clientID, tlsNoVerify)
if err != nil {
cli.ExitWithError("could not authenticate", err)
}
Expand All @@ -28,9 +28,9 @@ func auth_codeLogin(cmd *cobra.Command, args []string) {
AuthType: profiles.PROFILE_AUTH_TYPE_ACCESS_TOKEN,
AccessToken: profiles.AuthCredentialsAccessToken{
PublicClientID: publicClientID,
AccessToken: tok.AccessToken,
Expiration: tok.Expiry.Unix(),
RefreshToken: tok.RefreshToken,
AccessToken: tok.AccessToken,
Expiration: tok.Expiry.Unix(),
RefreshToken: tok.RefreshToken,
},
}); err != nil {
cli.ExitWithError("failed to set auth credentials", err)
Expand Down
8 changes: 7 additions & 1 deletion cmd/auth-logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ func auth_logout(cmd *cobra.Command, args []string) {
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 {
if err := auth.RevokeAccessToken(
cmd.Context(),
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)
}
Expand Down
16 changes: 13 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ func InitProfile(cmd *cobra.Command, onlyNew bool) *profiles.ProfileStore {
// TODO make this a preRun hook
func NewHandler(cmd *cobra.Command) handlers.Handler {
fh := cli.NewFlagHelper(cmd)

// Non-profile flags
host := fh.GetOptionalString("host")
tlsNoVerify := fh.GetOptionalBool("tls-no-verify")
withClientCreds := fh.GetOptionalString("with-client-creds")
withClientCredsFile := fh.GetOptionalString("with-client-creds-file")

// if global flags are set then validate and create a temporary profile in memory
var cp *profiles.ProfileStore
if host != "" || withClientCreds != "" || withClientCredsFile != "" {
err := errors.New("when using global flags --host, --with-client-creds, or --with-client-creds-file, " +
"profiles will not be used and all required flags must be set")
if host != "" || tlsNoVerify || withClientCreds != "" || withClientCredsFile != "" {
err := errors.New(
"when using global flags --host, --tls-no-verify, --with-client-creds, or --with-client-creds-file, " +
"profiles will not be used and all required flags must be set",
)

// host must be set
if host == "" {
Expand Down Expand Up @@ -170,6 +174,12 @@ func init() {
rootCmd.GetDocFlag("version").Description,
)

RootCmd.PersistentFlags().String(
rootCmd.GetDocFlag("profile").Name,
rootCmd.GetDocFlag("profile").Default,
rootCmd.GetDocFlag("profile").Description,
)

RootCmd.PersistentFlags().String(
rootCmd.GetDocFlag("host").Name,
rootCmd.GetDocFlag("host").Default,
Expand Down
3 changes: 3 additions & 0 deletions docs/man/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ command:
- name: version
description: show version
default: false
- name: profile
description: profile to use for interacting with the platform
default:
- name: host
description: Hostname of the platform (i.e. https://localhost)
default:
Expand Down
65 changes: 48 additions & 17 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ type platformConfiguration struct {
publicClientID string
}

type oidcClientCredentials struct {
clientID string
clientSecret string
isPublic bool
}

// Retrieves credentials by reading specified file
func GetClientCredsFromFile(filepath string) (ClientCredentials, error) {
creds := ClientCredentials{}
Expand Down Expand Up @@ -185,22 +191,19 @@ func GetTokenWithProfile(ctx context.Context, profile *profiles.ProfileStore) (*

// 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)
if err != nil && !errors.Is(err, sdk.ErrPlatformPublicClientIDNotFound) {
return nil, err
}

rp, err := oidcrp.NewRelyingPartyOIDC(ctx, pc.issuer, clientId, clientSecret, "", []string{"email"})
rp, err := newOidcRelyingParty(ctx, endpoint, tlsNoVerify, oidcClientCredentials{
clientID: clientId,
clientSecret: clientSecret,
})
if err != nil {
return nil, err
}

return oidcrp.ClientCredentials(ctx, rp, url.Values{})
}

// Facilitates an auth code PKCE flow to obtain OIDC tokens.
// Spawns a local server to handle the callback and opens a browser window in each respective OS.
func Login(platformEndpoint, tokenURL, authURL, publicClientID string) (*oauth2.Token, error) {
func Login(ctx context.Context, platformEndpoint, tokenURL, authURL, publicClientID string) (*oauth2.Token, error) {
// Generate random hash and encryption keys for cookie handling
hashKey := make([]byte, 16)
encryptKey := make([]byte, 16)
Expand All @@ -225,7 +228,6 @@ func Login(platformEndpoint, tokenURL, authURL, publicClientID string) (*oauth2.
},
}

ctx := context.Background()
cookiehandler := httphelper.NewCookieHandler(hashKey, encryptKey)

relyingParty, err := oidcrp.NewRelyingPartyOAuth(conf,
Expand All @@ -252,13 +254,13 @@ func Login(platformEndpoint, tokenURL, authURL, publicClientID string) (*oauth2.
}

// 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, string, error) {
func LoginWithPKCE(ctx context.Context, 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)
}

tok, err := Login(host, pc.tokenEndpoint, pc.authzEndpoint, pc.publicClientID)
tok, err := Login(ctx, host, pc.tokenEndpoint, pc.authzEndpoint, pc.publicClientID)
if err != nil {
return nil, "", fmt.Errorf("failed to login: %w", err)
}
Expand All @@ -267,16 +269,45 @@ func LoginWithPKCE(host, publicClientID string, tlsNoVerify bool) (*oauth2.Token
}

// Revokes the access token
func RevokeAccessToken(endpoint, publicClientID, refreshToken string, tlsNoVerify bool) error {
pCfg, err := getPlatformConfiguration(endpoint, publicClientID, tlsNoVerify)
func RevokeAccessToken(ctx context.Context, endpoint, publicClientID, refreshToken string, tlsNoVerify bool) error {
rp, err := newOidcRelyingParty(ctx, endpoint, tlsNoVerify, oidcClientCredentials{
clientID: publicClientID,
isPublic: true,
})
if err != nil {
return fmt.Errorf("failed to get platform configuration: %w", err)
return err
}
return oidcrp.RevokeToken(ctx, rp, refreshToken, "refresh_token")
}

rp, err := oidcrp.NewRelyingPartyOIDC(context.Background(), pCfg.issuer, pCfg.publicClientID, "", "", nil)
func newOidcRelyingParty(ctx context.Context, endpoint string, tlsNoVerify bool, clientCreds oidcClientCredentials) (oidcrp.RelyingParty, error) {
if clientCreds.clientID == "" {
return nil, errors.New("client ID is required")
}
if clientCreds.clientSecret == "" && !clientCreds.isPublic {
return nil, errors.New("client secret is required")
}
if clientCreds.clientSecret != "" && clientCreds.isPublic {
return nil, errors.New("client secret must be empty for public clients")
}

var pcClient string
if clientCreds.isPublic {
pcClient = clientCreds.clientID
}

pc, err := getPlatformConfiguration(endpoint, pcClient, tlsNoVerify)
if err != nil {
return err
return nil, err
}

return oidcrp.RevokeToken(context.Background(), rp, refreshToken, "refresh_token")
return oidcrp.NewRelyingPartyOIDC(
ctx,
pc.issuer,
clientCreds.clientID,
clientCreds.clientSecret,
"",
nil,
oidcrp.WithHTTPClient(utils.NewHttpClient(tlsNoVerify)),
)
}
16 changes: 16 additions & 0 deletions pkg/utils/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package utils

import (
"crypto/tls"
"net/http"
)

func NewHttpClient(tlsNoVerify bool) *http.Client {
return &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: tlsNoVerify,
},
},
}
}