Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CLI-2360] Revoking Auth0 refresh token on cloud logout #2216

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

kevin-wu24
Copy link
Contributor

@kevin-wu24 kevin-wu24 commented Aug 22, 2023

Release Notes

Bug Fixes

  • confluent logout revokes the refresh token when logging out of Confluent Cloud

Checklist

  1. [CRUCIAL] Is the change for features that are already live in prod?
    • yes: ok

What

Running confluent logout for cloud users now makes a request to DELETE /api/sessions.

References

https://confluentinc.atlassian.net/browse/CLI-2360
confluentinc/ccloud-sdk-go-v1-public#24

Test & Review

Unit and integration tests still pass
Request returns a reply with code 200 OK.

@kevin-wu24 kevin-wu24 requested review from a team as code owners August 22, 2023 20:01
internal/command.go Outdated Show resolved Hide resolved
internal/login/command_login_test.go Outdated Show resolved Hide resolved

func (a *AuthTokenHandlerImpl) RevokeRefreshToken(clientFactory CCloudClientFactory, url string, credentials *Credentials) error {
req := &ccloudv1.AuthenticateRequest{IdToken: credentials.AuthToken}
client := clientFactory.JwtHTTPClientFactory(context.Background(), credentials.AuthToken, url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't we just use the default c.Client in login/command.go?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned this in slack, but that was the reason I was getting Error 401. I was using the AnonHTTPClientFactory before, which I assume is what you mean by default client. However, the backend handler expects to have a userId in the header, so using the JwtHTTPClient worked.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, seems like this is due to confluent logout being an unauthenticated command. Maybe we should switch it to an authenticated command (we should be able to assume that the user is logged in before using it)

@@ -20,7 +20,7 @@ var (
mockURL = "http://test"
usernameCredentialName = fmt.Sprintf("username-%s-%s", mockEmail, mockURL)
apiKeyCredentialName = fmt.Sprintf("api-key-%s", kafkaAPIKey)
mockContextName = fmt.Sprintf("login-%s-%s", mockEmail, mockURL)
MockContextName = fmt.Sprintf("login-%s-%s", mockEmail, mockURL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where else is this var being used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a logout unit test.

loginCredentialsManager: loginCredentialsManager,
loginOrganizationManager: loginOrganizationManager,
authTokenHandler: authTokenHandler,
}
if cfg.IsCloudLogin() {
context = "Confluent Cloud"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
context = "Confluent Cloud"
context = "Confluent Cloud"
AuthenticatedCLICommand: pcmd.NewAuthenticatedCLICommand(cmd, prerunner),

move it down for readibility

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're not logged in, we still want to have this struct field set. I originally had the code this way, but it resulted in a panic rather than an error when the user was not logged in.

@MuweiHe
Copy link
Contributor

MuweiHe commented Aug 23, 2023

Have you manually tested, that IsSSO field in get ccloud credentials is always correct?

@kevin-wu24
Copy link
Contributor Author

kevin-wu24 commented Aug 23, 2023

Have you manually tested, that IsSSO field in get ccloud credentials is always correct?

yup! It's the same code that used to exist in login/command.go, but I just moved it to pkg since it is shared.

EDIT: did some refactoring and it turns out we don't even need to call the getCCloudCredentials function, since logout an authenticated command now.

@@ -811,10 +810,10 @@ func TestLoginWithExistingContext(t *testing.T) {
ctx.KafkaClusterContext.SetActiveKafkaCluster(kafkaCluster.ID)

// Executing logout
logoutCmd, _ := newLogoutCmd(cfg, mockNetrcHandler)
logoutCmd, cfg1 := newLogoutCmd(auth, userInterface, s.isCloud, req, mockNetrcHandler, AuthTokenHandler, mockLoginCredentialsManager, LoginOrganizationManager, ctx.Name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logoutCmd, cfg1 := newLogoutCmd(auth, userInterface, s.isCloud, req, mockNetrcHandler, AuthTokenHandler, mockLoginCredentialsManager, LoginOrganizationManager, ctx.Name)
logoutCmd, cfg := newLogoutCmd(auth, userInterface, s.isCloud, req, mockNetrcHandler, AuthTokenHandler, mockLoginCredentialsManager, LoginOrganizationManager, ctx.Name)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be changed so that the newLogoutCmd doesn't generate a new config, but use the same config as above.

@@ -903,7 +902,7 @@ func newLoginCmd(auth *ccloudv1mock.Auth, userInterface *ccloudv1mock.UserInterf
if !isCloud {
mdsConfig := mdsv1.NewConfiguration()
mdsClient = mdsv1.NewAPIClient(mdsConfig)
mdsClient.TokensAndAuthenticationApi = &mdsmock.TokensAndAuthenticationApi{
mdsClient.TokensAndAuthenticationApi = &mdsMock.TokensAndAuthenticationApi{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package names should always be lowercase.

Suggested change
mdsClient.TokensAndAuthenticationApi = &mdsMock.TokensAndAuthenticationApi{
mdsClient.TokensAndAuthenticationApi = &mdsmock.TokensAndAuthenticationApi{

@@ -937,8 +936,38 @@ func newLoginCmd(auth *ccloudv1mock.Auth, userInterface *ccloudv1mock.UserInterf
return loginCmd, cfg
}

func newLogoutCmd(cfg *config.Config, netrcHandler netrc.NetrcHandler) (*cobra.Command, *config.Config) {
logoutCmd := logout.New(cfg, climock.NewPreRunnerMock(nil, nil, nil, nil, cfg), netrcHandler)
func newLogoutCmd(auth *ccloudv1mock.Auth, userInterface *ccloudv1mock.UserInterface, isCloud bool, req *require.Assertions, netrcHandler netrc.NetrcHandler, authTokenHandler pauth.AuthTokenHandler, loginCredentialsManager pauth.LoginCredentialsManager, loginOrganizationManager pauth.LoginOrganizationManager, contextName string) (*cobra.Command, *config.Config) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a crazy number of arguments. Do we really need all of them?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I forgot to remove some of these, as they aren't used anymore.

mdsConfig := mdsv1.NewConfiguration()
mdsClient = mdsv1.NewAPIClient(mdsConfig)
mdsClient.TokensAndAuthenticationApi = &mdsMock.TokensAndAuthenticationApi{
GetTokenFunc: func(ctx context.Context) (mdsv1.AuthenticationResponse, *http.Response, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
GetTokenFunc: func(ctx context.Context) (mdsv1.AuthenticationResponse, *http.Response, error) {
GetTokenFunc: func(_ context.Context) (mdsv1.AuthenticationResponse, *http.Response, error) {

Comment on lines 948 to 952
return mdsv1.AuthenticationResponse{
AuthToken: testToken1,
TokenType: "JWT",
ExpiresIn: 100,
}, nil, nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return mdsv1.AuthenticationResponse{
AuthToken: testToken1,
TokenType: "JWT",
ExpiresIn: 100,
}, nil, nil
res := mdsv1.AuthenticationResponse{
AuthToken: testToken1,
TokenType: "JWT",
ExpiresIn: 100,
}
return res, nil, nil

@@ -169,3 +170,11 @@ func login(client *ccloudv1.Client, req *ccloudv1.AuthenticateRequest) (*ccloudv
return client.Auth.Login(req)
}
}

func (a *AuthTokenHandlerImpl) RevokeRefreshToken(client *ccloudv1.Client, req *ccloudv1.AuthenticateRequest) (*ccloudv1.AuthenticateReply, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this function need to be inside AuthTokenHandlerImpl? (I don't see a being used anywhere here) It looks like you're introducing a dependency for no reason.

},
}
}
ccloudClientFactory := &climock.CCloudClientFactory{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this should only run when isCloud is true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the NewPreRunnerMock function that takes in this variable runs whether or not isCloud is true.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like we should change that too?

ccloudClientFactory := &climock.CCloudClientFactory{
AnonHTTPClientFactoryFunc: func(baseURL string) *ccloudv1.Client {
req.Equal("https://confluent.cloud", baseURL)
return &ccloudv1.Client{Params: &ccloudv1.Params{HttpClient: new(http.Client)}, Auth: auth, User: userInterface}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return &ccloudv1.Client{Params: &ccloudv1.Params{HttpClient: new(http.Client)}, Auth: auth, User: userInterface}
return &ccloudv1.Client{
Params: &ccloudv1.Params{HttpClient: new(http.Client)},
Auth: auth,
User: userInterface,
}

@@ -64,3 +78,18 @@ func (c *Command) logout(cmd *cobra.Command, _ []string) error {
output.Println(errors.LoggedOutMsg)
return nil
}

func (c *command) revokeCCloudRefreshToken() (*ccloudv1.AuthenticateReply, error) {
ctx := c.Config.Config.Context()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why we need to do this? It looks wrong- can it be shortened?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can just pass ctx into the function.

}

req := &ccloudv1.AuthenticateRequest{IdToken: contextState.AuthToken}
if sso.IsOkta(c.Client.BaseURL) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get the URL from the configuration instead? This seems hacky

config.SetTempHomeDir()
cfg := config.AuthenticatedConfigMockWithContextName(contextName)
var mdsClient *mdsv1.APIClient
if !isCloud {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like duplicate code. Can we de-duplicate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a place in the code base where shared unit test helpers already exist? Looks like the ccloudClientFactory and mdsClient are used in both the login and logout unit tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet, but since these tests span two packages, I'd expect it to go somewhere in pkg/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried putting this in existing packages that made sense in pkg/ (auth and mock) but both result in circular imports, so I had to create a new directory unfortunately.

@@ -21,6 +18,7 @@ import (
"github.com/confluentinc/cli/v3/pkg/config"
pmock "github.com/confluentinc/cli/v3/pkg/mock"
"github.com/confluentinc/cli/v3/pkg/netrc"
testhelp "github.com/confluentinc/cli/v3/pkg/test-helpers"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
testhelp "github.com/confluentinc/cli/v3/pkg/test-helpers"
testhelpers "github.com/confluentinc/cli/v3/pkg/test-helpers"

Comment on lines 47 to 51
return mdsv1.AuthenticationResponse{
AuthToken: token,
TokenType: "JWT",
ExpiresIn: 100,
}, nil, nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return mdsv1.AuthenticationResponse{
AuthToken: token,
TokenType: "JWT",
ExpiresIn: 100,
}, nil, nil
res := mdsv1.AuthenticationResponse{
AuthToken: token,
TokenType: "JWT",
ExpiresIn: 100,
}
return res, nil, nil

Comment on lines 42 to 44
var mdsClient *mdsv1.APIClient
mdsConfig := mdsv1.NewConfiguration()
mdsClient = mdsv1.NewAPIClient(mdsConfig)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var mdsClient *mdsv1.APIClient
mdsConfig := mdsv1.NewConfiguration()
mdsClient = mdsv1.NewAPIClient(mdsConfig)
mdsConfig := mdsv1.NewConfiguration()
mdsClient := mdsv1.NewAPIClient(mdsConfig)

@@ -0,0 +1,55 @@
package test_helpers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_helpers is a really generic name and has the potential to get really big the more things we move into here. Why not put this in pkg/mock?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get a circular import unfortunately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try to fix it another way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can move it to mock, just not pkg/mock.

}

mdsClientManager := &climock.MDSClientManager{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise, this looks like it should only be declared inside the if

Comment on lines 939 to 940
logoutCmd := logout.New(cfg, prerunner, netrcHandler, authTokenHandler)
return logoutCmd
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logoutCmd := logout.New(cfg, prerunner, netrcHandler, authTokenHandler)
return logoutCmd
return logout.New(cfg, prerunner, netrcHandler, authTokenHandler)

username, err := c.netrcHandler.RemoveNetrcCredentials(c.cfg.IsCloudLogin(), c.Config.Config.Context().GetNetrcMachineName())
if err == nil {
log.CliLogger.Warnf(errors.RemoveNetrcCredentialsMsg, username, c.netrcHandler.GetFileName())
} else if !strings.Contains(err.Error(), "login credentials not found") && !strings.Contains(err.Error(), "keyword expected") {
// return err when other than NetrcCredentialsNotFoundErrorMsg or parsing error
return err
}

if isCCloud := ccloudv2.IsCCloudURL(ctx.Platform.Server, c.cfg.IsTest); isCCloud {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if isCCloud := ccloudv2.IsCCloudURL(ctx.Platform.Server, c.cfg.IsTest); isCCloud {
if ccloudv2.IsCCloudURL(ctx.Platform.Server, c.cfg.IsTest) {

contextName := cfg.Context().Name
logoutCmd, cfg := newLogoutCmd(cfg, mockNetrcHandler)
// run login command
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems wrong

@@ -190,6 +190,10 @@ func (r *PreRun) Authenticated(command *AuthenticatedCLICommand) func(*cobra.Com
setContextErr := r.setAuthenticatedContext(command)
if setContextErr != nil {
if _, ok := setContextErr.(*errors.NotLoggedInError); ok {
if cmd.Use == "logout" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any other errors that could surface and cause a breaking change? We may want to re-evaluate this approach. If not, let's keep as is, but definitely write a comment here explaining why (and open a ticket to remove this hack in v4)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried some other scenarios and it looks like other errors can surface. If the login token expires, logout throws an expired token error, and if you try logging out again after that you get the not logged in error again (which is because although our current context exists, but we're no longer considered logged into it). These errors result from the fact that we're trying to have logout work exactly the same as before when the user is not logged in, despite it now being an authenticatedCLICommand.

I think we should probably just wait until v4 to add this since it doesn't look like all the possible breaking changes from errors can be avoided without putting several of these hacky exceptions for the logout command in the prerunner.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably just wait until v4 to add this since it doesn't look like all the possible breaking changes from errors can be avoided without putting several of these hacky exceptions for the logout command in the prerunner.

Labeling as v4!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to revert the last two commits since we want the breaking changes to be visible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants