Skip to content

Commit

Permalink
Add initial refresh
Browse files Browse the repository at this point in the history
  • Loading branch information
filip-debricked committed Sep 13, 2024
1 parent 5df0f8d commit 059efc8
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 25 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-oauth2/oauth2 v3.9.2+incompatible // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ github.com/go-oauth2/oauth2 v3.9.2+incompatible h1:A8gSjq4110EgZDVk4ZtcpusynU2Ft
github.com/go-oauth2/oauth2 v3.9.2+incompatible/go.mod h1:GGcZ+i513KxN4yS7zBYfmwo3P+cyGvCS675uCNmWv/g=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down
48 changes: 37 additions & 11 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package auth

import (
"context"

"github.com/debricked/cli/internal/client"
"github.com/golang-jwt/jwt"
"github.com/zalando/go-keyring"
"golang.org/x/oauth2"
)
Expand All @@ -22,6 +24,7 @@ type ISecretClient interface {
type IOAuthConfig interface {
AuthCodeURL(string, ...oauth2.AuthCodeOption) string
Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error)
TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource
} // Wrapping interface for config to simplify mocking

type Authenticator struct {
Expand Down Expand Up @@ -74,6 +77,12 @@ func (a Authenticator) Logout() error {
return a.SecretClient.Delete("DebrickedAccessToken")
}

func validateJWT(token string) error {
claims := jwt.MapClaims{}
jwt.ParseWithClaims(token, claims, nil)
return claims.Valid()
}

func (a Authenticator) Token() (*oauth2.Token, error) {
refreshToken, err := a.SecretClient.Get("DebrickedRefreshToken")
if err != nil {
Expand All @@ -83,15 +92,21 @@ func (a Authenticator) Token() (*oauth2.Token, error) {
if err != nil {
return nil, err
}

jwtErr := validateJWT(accessToken)
if jwtErr != nil {
if jwtErr.Error() == "Token is expired" {
return a.refresh(refreshToken)
} else {
return nil, jwtErr
}
}
return &oauth2.Token{
RefreshToken: refreshToken,
TokenType: "jwt",
AccessToken: accessToken,
}, nil
}

func (a Authenticator) saveToken(token *oauth2.Token) error {
func (a Authenticator) save(token *oauth2.Token) error {
err := a.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken)
if err != nil {
return err
Expand All @@ -100,14 +115,21 @@ func (a Authenticator) saveToken(token *oauth2.Token) error {
return a.SecretClient.Set("DebrickedAccessToken", token.AccessToken)
}

func (a Authenticator) exchange(authCode, codeVerifier string) (*oauth2.Token, error) {

return a.OAuthConfig.Exchange(
func (a Authenticator) refresh(refreshToken string) (*oauth2.Token, error) {
token := &oauth2.Token{
RefreshToken: refreshToken,
}
tokenSource := a.OAuthConfig.TokenSource(
context.Background(),
authCode,
oauth2.VerifierOption(codeVerifier),
token,
)

token, err := tokenSource.Token()
if err != nil {
return nil, err
} else {
a.save(token)
return token, nil
}
}

func (a Authenticator) Authenticate() error {
Expand All @@ -124,10 +146,14 @@ func (a Authenticator) Authenticate() error {
}

authCode := a.AuthWebHelper.Callback(state)
token, err := a.exchange(authCode, codeVerifier)
token, err := a.OAuthConfig.Exchange(
context.Background(),
authCode,
oauth2.VerifierOption(codeVerifier),
)
if err != nil {
return err
}

return a.saveToken(token)
return a.save(token)
}
87 changes: 84 additions & 3 deletions internal/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ func TestSecretClientGet(t *testing.T) {
assert.Equal(t, secret, savedSecret)
}

func TestSecretClientGetExpired(t *testing.T) {
user := "TestDebrickedCLIUserGet"
service := "TestDebrickedCLIServiceGet"
secret := "TestDebrickedCLISecretGet"
dsc := DebrickedSecretClient{user}
err := keyring.Set(service, user, secret)
assert.NoError(t, err)
savedSecret, err := dsc.Get(service)
assert.NoError(t, err)
err = keyring.Delete(service, user)
assert.NoError(t, err)
assert.Equal(t, secret, savedSecret)
}

func TestSecretClientDelete(t *testing.T) {
user := "TestDebrickedCLIUserDelete"
service := "TestDebrickedCLIServiceDelete"
Expand Down Expand Up @@ -92,7 +106,7 @@ func TestMockedSaveToken(t *testing.T) {
RefreshToken: "refreshToken",
AccessToken: "accessToken",
}
err := authenticator.saveToken(token)
err := authenticator.save(token)

assert.NoError(t, err)
}
Expand All @@ -108,7 +122,7 @@ func TestMockedSaveTokenRefreshError(t *testing.T) {
RefreshToken: "refreshToken",
AccessToken: "accessToken",
}
err := authenticator.saveToken(token)
err := authenticator.save(token)

assert.Error(t, err)
}
Expand All @@ -121,11 +135,28 @@ func TestMockedToken(t *testing.T) {
token, err := authenticator.Token()

assert.NoError(t, err)
assert.Equal(t, token.TokenType, "jwt")
assert.Equal(t, token.RefreshToken, "token")
assert.Equal(t, token.AccessToken, "token")
}

func TestMockedTokenExpired(t *testing.T) {
authenticator := Authenticator{
SecretClient: testdata.MockExpiredSecretClient{},
OAuthConfig: testdata.MockOAuthConfig{
MockTokenSource: testdata.MockTokenSource{
StaticToken: &oauth2.Token{
RefreshToken: "refreshToken",
AccessToken: "accessToken",
},
Error: nil,
},
},
}
_, err := authenticator.Token()

assert.NoError(t, err)
}

func TestMockedTokenRefreshError(t *testing.T) {
authenticator := Authenticator{
SecretClient: testdata.MockErrorSecretClient{
Expand Down Expand Up @@ -161,6 +192,56 @@ func TestMockedAuthenticate(t *testing.T) {
assert.NoError(t, err)
}

func TestMockedAuthenticateExchangeError(t *testing.T) {
authenticator := Authenticator{
SecretClient: testdata.MockSecretClient{},
OAuthConfig: testdata.MockOAuthConfigExchangeError{},
AuthWebHelper: testdata.MockAuthWebHelper{},
}
err := authenticator.Authenticate()

assert.Error(t, err)
assert.Equal(t, "HTTP Error", err.Error())
}

func TestMockedRefresh(t *testing.T) {
authenticator := Authenticator{
SecretClient: testdata.MockSecretClient{},
OAuthConfig: testdata.MockOAuthConfig{
MockTokenSource: testdata.MockTokenSource{
StaticToken: &oauth2.Token{
RefreshToken: "refreshToken",
AccessToken: "accessToken",
},
Error: nil,
},
},
AuthWebHelper: testdata.MockAuthWebHelper{},
}
token, err := authenticator.refresh("refreshToken")

assert.NoError(t, err)
assert.Equal(t, "accessToken", token.AccessToken)
}

func TestMockedRefreshError(t *testing.T) {
authenticator := Authenticator{
SecretClient: testdata.MockSecretClient{},
OAuthConfig: testdata.MockOAuthConfig{
MockTokenSource: testdata.MockTokenSource{
StaticToken: nil,
Error: testdata.MockError{
Message: "testerror",
},
},
},
AuthWebHelper: testdata.MockAuthWebHelper{},
}
_, err := authenticator.refresh("refreshToken")

assert.Error(t, err)
}

func TestMockedAuthenticateOpenURLError(t *testing.T) {
authenticator := Authenticator{
SecretClient: testdata.MockSecretClient{},
Expand Down
87 changes: 78 additions & 9 deletions internal/auth/testdata/auth_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ import (
"golang.org/x/oauth2"
)

type MockError struct{}
type MockError struct {
Message string
}

func (me MockError) Error() string {
return "MockError!"
return me.Message
}

type MockSecretClient struct{}

type MockExpiredSecretClient struct{}

type MockInvalidSecretClient struct{}

func (msc MockSecretClient) Set(service, secret string) error {
return nil
}
Expand All @@ -27,14 +33,41 @@ func (msc MockSecretClient) Delete(service string) error {
return nil
}

func (msc MockExpiredSecretClient) Set(service, secret string) error {
return nil
}

func (msc MockExpiredSecretClient) Get(service string) (string, error) {
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMTkxOTQ2Mi03ZDZlLTc4ZTgtYWEyNC1iYTc3OTIxM2M5MGYiLCJqdGkiOiJlMTdhMmFlYTk0ZjgyNTdjYWU1NWM3ZjRiNTczNTRiMzI2YmNiYTZiZmY3ZGQ0ZWQ2NjU3NDA4MWE4ODFjN2VhMmM3OGU3Y2EzM2UxMjU5MyIsImlhdCI6MTY5NDU5NzkzNy4zNjAwMTUsIm5iZiI6MTY5NDU5NzkzNy4zNjAwMTcsImV4cCI6MTY5NDU5NzkzNy4zNTM3MDMsInN1YiI6ImZpbGlwLmhlZGVuK2FkbWluQGRlYnJpY2tlZC5jb20iLCJzY29wZXMiOlsic2VsZWN0IiwicHJvZmlsZSIsImJhc2ljUmVwbyJdfQ.CMqnQM9QFHTthDMv4K8q6gmkkFmbOIhrmKXwfo7kMWU", nil
}

func (msc MockExpiredSecretClient) Delete(service string) error {
return nil
}

func (msc MockInvalidSecretClient) Set(service, secret string) error {
return nil
}

func (msc MockInvalidSecretClient) Get(service string) (string, error) {
return "eyJhdWQiOiIwMTkxOTQ2Mi03ZDZlLTc4ZTgtYWEyNC1iYTc3OTIxM2M5MGYiLCJqdGkiOiJlMTdhMmFlYTk0ZjgyNTdjYWU1NWM3ZjRiNTczNTRiMzI2YmNiYTZiZmY3ZGQ0ZWQ2NjU3NDA4MWE4ODFjN2VhMmM3OGU3Y2EzM2UxMjU5MyIsImlhdCI6MTY5NDU5NzkzNy4zNjAwMTUsIm5iZiI6MTY5NDU5NzkzNy4zNjAwMTcsImV4cCI6MTY5NDU5NzkzNy4zNTM3MDMsInN1YiI6ImZpbGlwLmhlZGVuK2FkbWluQGRlYnJpY2tlZC5jb20iLCJzY29wZXMiOlsic2VsZWN0IiwicHJvZmlsZSIsImJhc2ljUmVwbyJdfQ", nil
}

func (msc MockInvalidSecretClient) Delete(service string) error {
return nil
}

type MockErrorSecretClient struct {
ErrorPattern string
Message string
}

func (msc MockErrorSecretClient) Set(service, secret string) error {
if strings.Contains(service, msc.ErrorPattern) {

return MockError{}
return MockError{
Message: msc.Message,
}
}

return nil
Expand All @@ -43,15 +76,19 @@ func (msc MockErrorSecretClient) Set(service, secret string) error {
func (msc MockErrorSecretClient) Get(service string) (string, error) {
if strings.Contains(service, msc.ErrorPattern) {

return "", MockError{}
return "", MockError{
Message: msc.Message,
}
}
return "token", nil
}

func (msc MockErrorSecretClient) Delete(service string) error {
if strings.Contains(service, msc.ErrorPattern) {

return MockError{}
return MockError{
Message: msc.Message,
}
}
return nil
}
Expand All @@ -60,7 +97,11 @@ type MockAuthenticator struct{}

type ErrorMockAuthenticator struct{}

type MockOAuthConfig struct{}
type MockOAuthConfig struct {
MockTokenSource oauth2.TokenSource
}

type MockOAuthConfigExchangeError struct{}

type MockAuthWebHelper struct{}

Expand All @@ -83,15 +124,15 @@ func (ma MockAuthenticator) Token() (*oauth2.Token, error) {
}

func (ma ErrorMockAuthenticator) Authenticate() error {
return MockError{}
return MockError{""}
}

func (ma ErrorMockAuthenticator) Logout() error {
return MockError{}
return MockError{""}
}

func (ma ErrorMockAuthenticator) Token() (*oauth2.Token, error) {
return nil, MockError{}
return nil, MockError{""}
}

func (mawh MockAuthWebHelper) OpenURL(string) error {
Expand Down Expand Up @@ -120,3 +161,31 @@ func (moc MockOAuthConfig) Exchange(context.Context, string, ...oauth2.AuthCodeO
func (moc MockOAuthConfig) AuthCodeURL(string, ...oauth2.AuthCodeOption) string {
return "localhost"
}

func (moc MockOAuthConfigExchangeError) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return nil, MockError{Message: "HTTP Error"}
}

func (moc MockOAuthConfigExchangeError) AuthCodeURL(string, ...oauth2.AuthCodeOption) string {
return "localhost"
}

func (moc MockOAuthConfigExchangeError) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
return nil
}

type MockTokenSource struct {
StaticToken *oauth2.Token
Error error
}

func (mts MockTokenSource) Token() (*oauth2.Token, error) {
if mts.Error != nil {
return nil, mts.Error
}
return mts.StaticToken, nil
}

func (moc MockOAuthConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
return moc.MockTokenSource
}
Loading

0 comments on commit 059efc8

Please sign in to comment.