Skip to content

Commit

Permalink
Merge pull request #775 from themooks/PKCE-Support-GC-CLI
Browse files Browse the repository at this point in the history
[DEVTOOLING-252] Add OAuth PKCE Support in GC CLI SDK
  • Loading branch information
carnellj-genesys committed Feb 1, 2024
2 parents d371bd2 + f21d97a commit 5922955
Show file tree
Hide file tree
Showing 9 changed files with 258 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ const (
None GrantType = "0"
ClientCredentials = "1"
ImplicitGrant = "2"
PKCEGrant = "3"
)

func isValidGrantType(t GrantType) bool {
return t == None || t == ClientCredentials || t == ImplicitGrant
return t == None || t == ClientCredentials || t == ImplicitGrant || t == PKCEGrant
}

func constructConfig(profileName string, environment string, clientID string, clientSecret string, redirectURI string, secureLoginEnabled bool, accessToken string, proxyConf *config.ProxyConfiguration) config.Configuration {
func constructConfig(profileName string, environment string, grantType GrantType, clientID string, clientSecret string, redirectURI string, secureLoginEnabled bool, accessToken string, proxyConf *config.ProxyConfiguration) config.Configuration {
c := &mocks.MockClientConfig{}

c.ProfileNameFunc = func() string {
Expand All @@ -52,6 +53,10 @@ func constructConfig(profileName string, environment string, clientID string, cl
return false
}

c.GrantTypeFunc = func() string {
return fmt.Sprintf("%s", grantType)
}

c.ClientIDFunc = func() string {
return clientID
}
Expand Down Expand Up @@ -117,7 +122,7 @@ func requestUserInput() config.Configuration {

for true {
fmt.Print("Select your authorization grant type.\n")
fmt.Print("\t0. None\n\t1. Client Credentials\n\t2. Implicit Grant\nGrant Type: ")
fmt.Print("\t0. None\n\t1. Client Credentials\n\t2. Implicit Grant\n\t3. PKCE Grant\nGrant Type: ")
fmt.Scanln(&grantType)

if accessToken == "" && grantType == None {
Expand All @@ -131,7 +136,7 @@ func requestUserInput() config.Configuration {

clientID, clientSecret = requestClientCreds(accessToken, grantType)

if grantType == ImplicitGrant {
if (grantType == ImplicitGrant) || (grantType == PKCEGrant) {
redirectURL.Host = "localhost:" + requestRedirectURIPort()
for true {
fmt.Print("Would you like to use a secure HTTP connection? [Y/N]: ")
Expand All @@ -154,18 +159,18 @@ func requestUserInput() config.Configuration {
fmt.Scanln(&proxyChoice)
if strings.ToUpper(proxyChoice) == "Y" {
proxyConf := requestProxyDetails()
return constructConfig(name, environment, clientID, clientSecret, redirectURL.String(), secureLoginEnabled, accessToken, proxyConf)
return constructConfig(name, environment, grantType, clientID, clientSecret, redirectURL.String(), secureLoginEnabled, accessToken, proxyConf)
break
} else if strings.ToUpper(proxyChoice) == "N" {
return constructConfig(name, environment, clientID, clientSecret, redirectURL.String(), secureLoginEnabled, accessToken, nil)
return constructConfig(name, environment, grantType, clientID, clientSecret, redirectURL.String(), secureLoginEnabled, accessToken, nil)
break
} else {
fmt.Print("Provide valid option.\n")
continue
}
}

return constructConfig(name, environment, clientID, clientSecret, redirectURL.String(), secureLoginEnabled, accessToken, nil)
return constructConfig(name, environment, grantType, clientID, clientSecret, redirectURL.String(), secureLoginEnabled, accessToken, nil)
}

func requestClientCreds(accessToken string, grantType GrantType) (string, string) {
Expand Down Expand Up @@ -198,6 +203,12 @@ func requestClientCreds(accessToken string, grantType GrantType) (string, string

fmt.Print("Client Secret (Optional): ")
secret = readSensitiveInput()
} else if grantType == PKCEGrant {
// PKCE Grant
for id == "" {
fmt.Print("Client ID: ")
fmt.Scanln(&id)
}
}

return id, secret
Expand Down
22 changes: 21 additions & 1 deletion resources/sdk/clisdkclient/extensions/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
type Configuration interface {
ProfileName() string
Environment() string
GrantType() string
ClientID() string
ClientSecret() string
RedirectURI() string
Expand All @@ -32,6 +33,7 @@ type Configuration interface {
type configuration struct {
profileName string
environment string
grantType string
clientID string
clientSecret string
secureLoginEnabled bool
Expand Down Expand Up @@ -82,6 +84,11 @@ func (c *configuration) ProfileName() string {
return c.profileName
}

// GrantType is the OAuth grant type used by the OAuth Client
func (c *configuration) GrantType() string {
return viper.GetString(fmt.Sprintf("%s.grant_type", c.profileName))
}

// ClientID is the OAuth client id used by the OAuth Client
func (c *configuration) ClientID() string {
if ClientId != "" {
Expand Down Expand Up @@ -183,7 +190,7 @@ func getProxyConfig(profileName string) *ProxyConfiguration {
}

func (c *configuration) String() string {
return fmt.Sprintf(`{"profileName": "%s", "environment": "%s", "logFilePath": "%s", "loggingEnabled": "%v", "clientName": "%s", "clientSecret": "%s", "secureLoginEnabled": "%v", "redirectURI": "%s", "accessToken": "%s", "autoPaginationEnabled": "%v","proxyConfiguration": "%s"}`, c.ProfileName(), c.Environment(), c.LogFilePath(), c.LoggingEnabled(), c.ClientID(), c.ClientSecret(), c.SecureLoginEnabled(), c.RedirectURI(), c.AccessToken(), c.AutoPaginationEnabled(), getProxyConfig(c.ProfileName()).String())
return fmt.Sprintf(`{"profileName": "%s", "environment": "%s", "logFilePath": "%s", "loggingEnabled": "%v", "grantType": "%s", "clientName": "%s", "clientSecret": "%s", "secureLoginEnabled": "%v", "redirectURI": "%s", "accessToken": "%s", "autoPaginationEnabled": "%v","proxyConfiguration": "%s"}`, c.ProfileName(), c.Environment(), c.LogFilePath(), c.LoggingEnabled(), c.GrantType(), c.ClientID(), c.ClientSecret(), c.SecureLoginEnabled(), c.RedirectURI(), c.AccessToken(), c.AutoPaginationEnabled(), getProxyConfig(c.ProfileName()).String())
}

func (a *ProxyConfiguration) String() string {
Expand Down Expand Up @@ -256,6 +263,7 @@ func GetConfig(profileName string) (Configuration, error) {
}

return &configuration{profileName: profileName,
grantType: viper.GetString(fmt.Sprintf("%s.grant_type", profileName)),
clientID: viper.GetString(fmt.Sprintf("%s.client_credentials", profileName)),
clientSecret: viper.GetString(fmt.Sprintf("%s.client_secret", profileName)),
redirectURI: viper.GetString(fmt.Sprintf("%s.redirect_uri", profileName)),
Expand Down Expand Up @@ -290,6 +298,7 @@ func ListConfigs() ([]configuration, error) {
for profileName, _ := range settings {
configurations = append(configurations, configuration{
profileName: profileName,
grantType: viper.GetString(fmt.Sprintf("%s.grant_type", profileName)),
clientID: viper.GetString(fmt.Sprintf("%s.client_credentials", profileName)),
clientSecret: viper.GetString(fmt.Sprintf("%s.client_secret", profileName)),
redirectURI: viper.GetString(fmt.Sprintf("%s.redirect_uri", profileName)),
Expand Down Expand Up @@ -318,6 +327,13 @@ func UpdateOAuthToken(c Configuration, data *models.OAuthTokenData) error {
}, nil, nil, nil)
}

func UpdateGrantType(c Configuration, grantType string) error {
return updateConfig(configuration{
profileName: c.ProfileName(),
grantType: grantType,
}, nil, nil, nil)
}

func UpdateLogFilePath(c Configuration, filePath string) error {
return updateConfig(configuration{
profileName: c.ProfileName(),
Expand Down Expand Up @@ -397,6 +413,9 @@ func OverridesApplied() bool {
}

func updateConfig(c configuration, loggingEnabled *bool, autoPaginationEnabled *bool, secureLoginEnabled *bool) error {
if c.grantType != "" {
viper.Set(fmt.Sprintf("%s.grant_type", c.profileName), c.grantType)
}
if c.clientID != "" {
viper.Set(fmt.Sprintf("%s.client_credentials", c.profileName), c.clientID)
}
Expand Down Expand Up @@ -455,6 +474,7 @@ func updateConfig(c configuration, loggingEnabled *bool, autoPaginationEnabled *
}

func writeConfig(c Configuration, data *models.OAuthTokenData, logFilePath string, loggingEnabled *bool, autoPaginationEnabled *bool) error {
viper.Set(fmt.Sprintf("%s.grant_type", c.ProfileName()), c.GrantType())
viper.Set(fmt.Sprintf("%s.client_credentials", c.ProfileName()), c.ClientID())
viper.Set(fmt.Sprintf("%s.client_secret", c.ProfileName()), c.ClientSecret())
viper.Set(fmt.Sprintf("%s.redirect_uri", c.ProfileName()), c.RedirectURI())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
type MockClientConfig struct {
ProfileNameFunc func() string
EnvironmentFunc func() string
GrantTypeFunc func() string
ClientIDFunc func() string
ClientSecretFunc func() string
RedirectURIFunc func() string
Expand All @@ -32,6 +33,10 @@ func (m *MockClientConfig) Environment() string {
return m.EnvironmentFunc()
}

func (m *MockClientConfig) GrantType() string {
return m.GrantTypeFunc()
}

func (m *MockClientConfig) ClientID() string {
return m.ClientIDFunc()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
)

type ImplicitWebpage struct {}
type OAuthWebpage struct {}

type AuthResponse struct {
Title string
Expand All @@ -13,11 +13,15 @@ type AuthResponse struct {

var responses = initResponseMessages()

func (w ImplicitWebpage) GetImplicitWebpage(clientID string, environment string, redirectURI string) string {
return fmt.Sprintf(AuthWebpage, styles, clientID, redirectURI, environment, yetiImage)
func (w OAuthWebpage) GetImplicitWebpage(clientID string, environment string, redirectURI string) string {
return fmt.Sprintf(ImplicitAuthWebpage, styles, clientID, redirectURI, environment, yetiImage)
}

func (w ImplicitWebpage) GetResponsePage(result string) string {
func (w OAuthWebpage) GetPKCEWebpage(clientID string, environment string, redirectURI string, codeChallenge string) string {
return fmt.Sprintf(PKCEAuthWebpage, styles, clientID, redirectURI, codeChallenge, environment, yetiImage)
}

func (w OAuthWebpage) GetResponsePage(result string) string {
if res, ok := responses[result]; ok {
return fmt.Sprintf(ResponsePage, res.Title, styles, res.Title, yetiImage, res.Message)
} else {
Expand Down Expand Up @@ -60,10 +64,14 @@ func initResponseMessages() map[string]AuthResponse {
"503 Service Unavailable", "The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.",
}

r["invalid_pkce_package"] = AuthResponse{
"Invalid PKCE", "Invalid PKCE.",
}

return r
}

const AuthWebpage = `
const ImplicitAuthWebpage = `
<html>
<meta http-equiv="refresh" content="2"/>
<head>
Expand Down Expand Up @@ -106,13 +114,53 @@ const AuthWebpage = `
</html>
`

const PKCEAuthWebpage = `
<html>
<meta http-equiv="refresh" content="2"/>
<head>
<title>PKCE Login</title>
<script src="https://sdk-cdn.mypurecloud.com/external/go-cli/axios/0.23.0/axios.min.js"></script>
%s
<script>
if(window.location.search) {
let urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("error")) {
axios.get("/error/" + urlParams.get('error'));
} else {
let code = urlParams.get('code');
axios.get("/code/" + urlParams.get('code'));
}
} else {
var queryStringData = {
response_type : "code",
client_id : "%s",
redirect_uri : "%s",
code_challenge : "%s",
code_challenge_method: "S256"
}
let encodedURL = new URLSearchParams(queryStringData);
window.location.replace("https://login.%s/oauth/authorize?" + encodedURL.toString());
}
</script>

</head>
<body>
<p><span id="title">Loading...</span></p>
<p>
%s
</p>
</body>
</html>
`

const ResponsePage = `
<html>
<head>
<title>%s</title>
%s
<script>
window.location.hash = '';
window.history.pushState('Response', 'Response', '/');
</script>
</head>
<body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type apiClientTest struct {
func TestAuthorize(t *testing.T) {
UpdateOAuthToken = mocks.UpdateOAuthToken

mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "", false, utils.GenerateGuid(), utils.GenerateGuid(), "")
mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "", false, "1", utils.GenerateGuid(), utils.GenerateGuid(), "")
accessToken := "aJdvugb8k1kwnOovm2qX6LXTctJksYvdzcoXPrRDi-nL1phQhcKRN-bjcflq7CUDOmUCQv5OWuBSkPQr0peWhw"
setRestClientDoMockForAuthorize(t, *mockConfig, accessToken)

Expand All @@ -47,7 +47,7 @@ func TestAuthorize(t *testing.T) {
}

// Check that the same token is returned when the expiry time stamp is in the future
mockConfig = buildMockConfig(mockConfig.ProfileName(), mockConfig.Environment(), mockConfig.RedirectURI(), false, mockConfig.ClientID(), mockConfig.ClientSecret(), oauthData.String())
mockConfig = buildMockConfig(mockConfig.ProfileName(), mockConfig.Environment(), mockConfig.RedirectURI(), false, "1", mockConfig.ClientID(), mockConfig.ClientSecret(), oauthData.String())
oauthData, err = Authorize(mockConfig)
if err != nil {
t.Fatalf("err should be nil, got: %s", err)
Expand All @@ -59,7 +59,7 @@ func TestAuthorize(t *testing.T) {
// Check that a new token is retrieved when the expiry time stamp is in the past
oauthData.OAuthTokenExpiry = time.Now().AddDate(0, 0, -1).Format(time.RFC3339)
accessToken = "aJdvugb8k1kwnOovm2qX6LXTctJksYvdzcoXPrRDi-nL1phQhcKRN-bjcflq7CUDOmUCQv5OWuBSkPQr0peWhw"
mockConfig = buildMockConfig(mockConfig.ProfileName(), mockConfig.Environment(), mockConfig.RedirectURI(), false, mockConfig.ClientID(), mockConfig.ClientSecret(), oauthData.String())
mockConfig = buildMockConfig(mockConfig.ProfileName(), mockConfig.Environment(), mockConfig.RedirectURI(), false, "1", mockConfig.ClientID(), mockConfig.ClientSecret(), oauthData.String())
oauthData, err = Authorize(mockConfig)
if err != nil {
t.Fatalf("err should be nil, got: %s", err)
Expand All @@ -72,7 +72,7 @@ func TestAuthorize(t *testing.T) {
func TestAuthorizeWithImplicitLogin(t *testing.T) {
UpdateOAuthToken = mocks.UpdateOAuthToken

mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "http://localhost:8080", false, utils.GenerateGuid(), utils.GenerateGuid(), "")
mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "http://localhost:8080", false, "2", utils.GenerateGuid(), utils.GenerateGuid(), "")
accessToken := "aJdvugb8k1kwnOovm2qX6LXTctJksYvdzcoXPrRDi-nL1phQhcKRN-bjcflq7CUDOmUCQv5OWuBSkPQr0peWhw"
setRestClientDoMockForAuthorize(t, *mockConfig, accessToken)

Expand All @@ -94,7 +94,7 @@ func TestAuthorizeWithImplicitLogin(t *testing.T) {
}

// testing with secure http enabled
mockConfig = buildMockConfig("DEFAULT", "mypurecloud.com", "http://localhost:8080", true, utils.GenerateGuid(), utils.GenerateGuid(), "")
mockConfig = buildMockConfig("DEFAULT", "mypurecloud.com", "http://localhost:8080", true, "2", utils.GenerateGuid(), utils.GenerateGuid(), "")

oauthData, err = Authorize(mockConfig)
if err != nil {
Expand All @@ -112,7 +112,7 @@ func TestAuthorizeWithImplicitLogin(t *testing.T) {

func TestLowLevelRestClient(t *testing.T) {
tests := buildTestCaseTable()
mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "", false, utils.GenerateGuid(), utils.GenerateGuid(), "")
mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "", false, "0", utils.GenerateGuid(), utils.GenerateGuid(), "")

for _, tc := range tests {
restClient := &RESTClient{
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestLowLevelRestClient(t *testing.T) {

func TestHighLevelRestClient(t *testing.T) {
tests := buildTestCaseTable()
mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "", false, utils.GenerateGuid(), utils.GenerateGuid(), "")
mockConfig := buildMockConfig("DEFAULT", "mypurecloud.com", "", false, "0", utils.GenerateGuid(), utils.GenerateGuid(), "")

for _, tc := range tests {
restClient := &RESTClient{
Expand Down Expand Up @@ -184,7 +184,7 @@ func TestHighLevelRestClient(t *testing.T) {
}
}

func buildMockConfig(profileName string, environment string, redirectURI string, secureLoginEnabled bool, clientID string, clientSecret string, oauthTokenData string) *mocks.MockClientConfig {
func buildMockConfig(profileName string, environment string, redirectURI string, secureLoginEnabled bool, grantType string, clientID string, clientSecret string, oauthTokenData string) *mocks.MockClientConfig {
mockConfig := &mocks.MockClientConfig{}

mockConfig.ProfileNameFunc = func() string {
Expand Down Expand Up @@ -215,6 +215,10 @@ func buildMockConfig(profileName string, environment string, redirectURI string,
return secureLoginEnabled
}

mockConfig.GrantTypeFunc = func() string {
return grantType
}

mockConfig.ClientIDFunc = func() string {
return clientID
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ func mockGetConfig(profileName string) (config.Configuration, error) {
return false
}

mockConfig.GrantTypeFunc = func() string {
return "1"
}

mockConfig.ClientIDFunc = func() string {
return utils.GenerateGuid()
}
Expand Down Expand Up @@ -339,6 +343,10 @@ func mockGetConfigWithAccessToken(profileName string) (config.Configuration, err
return false
}

mockConfig.GrantTypeFunc = func() string {
return "0"
}

mockConfig.ClientIDFunc = func() string {
return ""
}
Expand Down
Loading

0 comments on commit 5922955

Please sign in to comment.