diff --git a/cmd/create.go b/cmd/create.go
index 3d404fa..7a9baf5 100644
--- a/cmd/create.go
+++ b/cmd/create.go
@@ -5,18 +5,13 @@ import (
gojson "encoding/json"
"fmt"
"github.com/elasticpath/epcc-cli/external/aliases"
- "github.com/elasticpath/epcc-cli/external/autofill"
"github.com/elasticpath/epcc-cli/external/completion"
- "github.com/elasticpath/epcc-cli/external/encoding"
"github.com/elasticpath/epcc-cli/external/httpclient"
"github.com/elasticpath/epcc-cli/external/json"
"github.com/elasticpath/epcc-cli/external/resources"
- "github.com/elasticpath/epcc-cli/external/shutdown"
+ "github.com/elasticpath/epcc-cli/external/rest"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
- "io"
- "net/http"
- "net/url"
"strings"
)
@@ -105,7 +100,7 @@ func NewCreateCommand(parentCmd *cobra.Command) func() {
}
}
- body, err := createInternal(context.Background(), overrides, append([]string{resourceName}, args...), autoFillOnCreate, setAlias, skipAliases)
+ body, err := rest.CreateInternal(context.Background(), overrides, append([]string{resourceName}, args...), autoFillOnCreate, setAlias, skipAliases)
if err != nil {
return err
@@ -241,126 +236,3 @@ func NewCreateCommand(parentCmd *cobra.Command) func() {
return resetFunc
}
-
-func createInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string, autoFillOnCreate bool, aliasName string, skipAliases bool) (string, error) {
- shutdown.OutstandingOpCounter.Add(1)
- defer shutdown.OutstandingOpCounter.Done()
-
- // Find Resource
- resource, ok := resources.GetResourceByName(args[0])
- if !ok {
- return "", fmt.Errorf("could not find resource %s", args[0])
- }
-
- if resource.CreateEntityInfo == nil {
- return "", fmt.Errorf("resource %s doesn't support CREATE", args[0])
- }
-
- // Count ids in CreateEntity
- resourceURL := resource.CreateEntityInfo.Url
-
- idCount, err := resources.GetNumberOfVariablesNeeded(resourceURL)
-
- if err != nil {
- return "", err
- }
-
- // Replace ids with args in resourceURL
- resourceURL, err = resources.GenerateUrl(resource.CreateEntityInfo, args[1:], true)
-
- if overrides.OverrideUrlPath != "" {
- log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath)
- resourceURL = overrides.OverrideUrlPath
- }
-
- if err != nil {
- return "", err
- }
-
- var resp *http.Response = nil
- var resBody []byte
-
- if resource.CreateEntityInfo.ContentType == "multipart/form-data" {
-
- byteBuf, contentType, err := encoding.ToMultiPartEncoding(args[(idCount+1):], resource.NoWrapping, resource.JsonApiFormat == "complaint", resource.Attributes)
- if err != nil {
- return "", err
- }
-
- // Submit request
- resp, err = httpclient.DoFileRequest(ctx, resourceURL, byteBuf, contentType)
-
- } else {
- // Assume it's application/json
-
- params := url.Values{}
-
- for _, v := range overrides.QueryParameters {
- keyAndValue := strings.SplitN(v, "=", 2)
- if len(keyAndValue) != 2 {
- return "", fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue)
- }
- params.Add(keyAndValue[0], keyAndValue[1])
- }
-
- if !resource.NoWrapping {
- args = append(args, "type", resource.JsonApiType)
- }
- // Create the body from remaining args
-
- jsonArgs := args[(idCount + 1):]
- if autoFillOnCreate {
- autofilledData := autofill.GetJsonArrayForResource(&resource)
-
- jsonArgs = append(autofilledData, jsonArgs...)
- }
-
- body, err := json.ToJson(jsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true)
-
- if err != nil {
- return "", err
- }
-
- // Submit request
- resp, err = httpclient.DoRequest(ctx, "POST", resourceURL, params.Encode(), strings.NewReader(body))
-
- }
-
- if err != nil {
- return "", fmt.Errorf("got error %s", err.Error())
- } else if resp == nil {
- return "", fmt.Errorf("got nil response with request: %s", resourceURL)
- }
-
- if resp.Body != nil {
- defer resp.Body.Close()
-
- // Print the body
- resBody, err = io.ReadAll(resp.Body)
- if err != nil {
- log.Fatal(err)
- }
-
- // Check if error response
- if resp.StatusCode >= 400 && resp.StatusCode <= 600 {
- json.PrintJson(string(resBody))
- return "", fmt.Errorf(resp.Status)
- }
-
- // 204 is no content, so we will skip it.
- if resp.StatusCode != 204 {
- if !skipAliases {
- aliases.SaveAliasesForResources(string(resBody))
- }
- }
-
- if aliasName != "" {
- aliases.SetAliasForResource(string(resBody), aliasName)
- }
-
- return string(resBody), nil
- } else {
- return "", nil
- }
-
-}
diff --git a/cmd/delete.go b/cmd/delete.go
index eaafd5e..d42aced 100644
--- a/cmd/delete.go
+++ b/cmd/delete.go
@@ -8,13 +8,9 @@ import (
"github.com/elasticpath/epcc-cli/external/httpclient"
"github.com/elasticpath/epcc-cli/external/json"
"github.com/elasticpath/epcc-cli/external/resources"
- "github.com/elasticpath/epcc-cli/external/shutdown"
+ "github.com/elasticpath/epcc-cli/external/rest"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
- "io"
- "net/http"
- "net/url"
- "strings"
)
func NewDeleteCommand(parentCmd *cobra.Command) func() {
@@ -94,7 +90,7 @@ func NewDeleteCommand(parentCmd *cobra.Command) func() {
}
}
- body, err := deleteInternal(context.Background(), overrides, allow404, append([]string{resourceName}, args...))
+ body, err := rest.DeleteInternal(context.Background(), overrides, allow404, append([]string{resourceName}, args...))
if err != nil {
if body != "" {
@@ -193,111 +189,3 @@ func NewDeleteCommand(parentCmd *cobra.Command) func() {
return resetFunc
}
-func deleteInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, allow404 bool, args []string) (string, error) {
- shutdown.OutstandingOpCounter.Add(1)
- defer shutdown.OutstandingOpCounter.Done()
-
- resource, ok := resources.GetResourceByName(args[0])
- if !ok {
- return "", fmt.Errorf("could not find resource %s", args[0])
- }
-
- resp, err := deleteResource(ctx, overrides, args)
- if err != nil {
- return "", err
- }
-
- if resp == nil {
- return "", fmt.Errorf("got nil response")
- }
-
- if resp.StatusCode < 400 {
- idToDelete := aliases.ResolveAliasValuesOrReturnIdentity(resource.JsonApiType, resource.AlternateJsonApiTypesForAliases, args[len(args)-1], "id")
- aliases.DeleteAliasesById(idToDelete, resource.JsonApiType)
- }
- if resp.Body != nil {
-
- defer resp.Body.Close()
-
- // Print the body
- body, err := io.ReadAll(resp.Body)
-
- if err != nil {
- log.Fatal(err)
- }
-
- // Check if error response
- if resp.StatusCode >= 400 && resp.StatusCode <= 600 {
- if resp.StatusCode != 404 || !allow404 {
- return string(body), fmt.Errorf(resp.Status)
- }
- }
-
- return string(body), nil
- } else {
- return "", nil
- }
-
-}
-
-func deleteResource(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (*http.Response, error) {
- // Find Resource
- resource, ok := resources.GetResourceByName(args[0])
- if !ok {
- return nil, fmt.Errorf("could not find resource %s", args[0])
- }
-
- if resource.DeleteEntityInfo == nil {
- return nil, fmt.Errorf("resource %s doesn't support DELETE", args[0])
- }
-
- // Replace ids with args in resourceURL
- resourceURL, err := resources.GenerateUrl(resource.DeleteEntityInfo, args[1:], true)
-
- if err != nil {
- return nil, err
- }
-
- if overrides.OverrideUrlPath != "" {
- log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath)
- resourceURL = overrides.OverrideUrlPath
- }
-
- params := url.Values{}
-
- for _, v := range overrides.QueryParameters {
- keyAndValue := strings.SplitN(v, "=", 2)
- if len(keyAndValue) != 2 {
- return nil, fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue)
- }
- params.Add(keyAndValue[0], keyAndValue[1])
- }
-
- idCount, err := resources.GetNumberOfVariablesNeeded(resource.DeleteEntityInfo.Url)
-
- if !resource.NoWrapping {
- args = append(args, "type", resource.JsonApiType)
- }
- // Create the body from remaining args
-
- jsonArgs := args[(idCount + 1):]
-
- var payload io.Reader = nil
- if len(jsonArgs) > 0 {
- body, err := json.ToJson(jsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true)
-
- if err != nil {
- return nil, err
- }
-
- payload = strings.NewReader(body)
- }
-
- // Submit request
- resp, err := httpclient.DoRequest(ctx, "DELETE", resourceURL, params.Encode(), payload)
- if err != nil {
- return nil, fmt.Errorf("got error %s", err.Error())
- }
-
- return resp, nil
-}
diff --git a/cmd/get.go b/cmd/get.go
index d39d9e8..73e50fc 100644
--- a/cmd/get.go
+++ b/cmd/get.go
@@ -9,13 +9,9 @@ import (
"github.com/elasticpath/epcc-cli/external/httpclient"
"github.com/elasticpath/epcc-cli/external/json"
"github.com/elasticpath/epcc-cli/external/resources"
- "github.com/elasticpath/epcc-cli/external/shutdown"
+ "github.com/elasticpath/epcc-cli/external/rest"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
- "io"
- "net/http"
- "net/url"
- "strings"
"time"
)
@@ -149,7 +145,7 @@ func NewGetCommand(parentCmd *cobra.Command) func() {
retriesFailedError := fmt.Errorf("Maximum number of retries hit %d and condition [%s] always true", retryWhileJQMaxAttempts, retryWhileJQ)
for attempt := uint16(0); attempt < retryWhileJQMaxAttempts; attempt++ {
- body, err = getInternal(context.Background(), overrides, append([]string{resourceName}, args...), skipAliases)
+ body, err = rest.GetInternal(context.Background(), overrides, append([]string{resourceName}, args...), skipAliases)
if retryWhileJQ == "" {
retriesFailedError = nil
break
@@ -305,117 +301,3 @@ func NewGetCommand(parentCmd *cobra.Command) func() {
return resetFunc
}
-
-func getInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string, skipAliases bool) (string, error) {
- resp, err := getResource(ctx, overrides, args)
-
- if err != nil {
- return "", err
- } else if resp == nil {
- return "", fmt.Errorf("got nil response")
- }
-
- if resp.Body != nil {
- defer resp.Body.Close()
-
- // Print the body
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- log.Fatal(err)
- }
-
- // Check if error response
- if resp.StatusCode >= 400 && resp.StatusCode <= 600 {
- json.PrintJson(string(body))
- return "", fmt.Errorf(resp.Status)
- }
-
- if !skipAliases {
- aliases.SaveAliasesForResources(string(body))
- }
-
- return string(body), nil
- } else {
- return "", nil
- }
-}
-
-func getUrl(resource resources.Resource, args []string) (*resources.CrudEntityInfo, error) {
-
- if resource.GetCollectionInfo == nil && resource.GetEntityInfo == nil {
- return nil, fmt.Errorf("resource %s doesn't support GET", args[0])
- } else if resource.GetCollectionInfo != nil && resource.GetEntityInfo == nil {
- return resource.GetCollectionInfo, nil
- } else if resource.GetCollectionInfo == nil && resource.GetEntityInfo != nil {
- return resource.GetEntityInfo, nil
- } else {
- if _, ok := resources.GetPluralResources()[args[0]]; ok {
- return resource.GetCollectionInfo, nil
- } else {
- return resource.GetEntityInfo, nil
- }
- }
-}
-
-func getResource(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (*http.Response, error) {
- shutdown.OutstandingOpCounter.Add(1)
- defer shutdown.OutstandingOpCounter.Done()
-
- // Find Resource
- resource, ok := resources.GetResourceByName(args[0])
- if !ok {
- return nil, fmt.Errorf("could not find resource %s", args[0])
- }
-
- var idCount int
-
- resourceUrlInfo, err2 := getUrl(resource, args)
- if err2 != nil {
- return nil, err2
- }
-
- idCount, err := resources.GetNumberOfVariablesNeeded(resourceUrlInfo.Url)
-
- if err != nil {
- return nil, err
- }
-
- // Replace ids with args in resourceURL
- resourceURL, err := resources.GenerateUrl(resourceUrlInfo, args[1:], true)
-
- if err != nil {
- return nil, err
- }
-
- if overrides.OverrideUrlPath != "" {
- log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath)
- resourceURL = overrides.OverrideUrlPath
- }
-
- // Add remaining args as query params
- params := url.Values{}
- for i := idCount + 1; i+1 < len(args); i = i + 2 {
- params.Add(args[i], args[i+1])
- }
-
- if (idCount-len(args)+1)%2 != 0 {
- log.Warnf("Extra argument at the end of the command %s", args[len(args)-1])
- }
-
- for _, v := range overrides.QueryParameters {
- keyAndValue := strings.SplitN(v, "=", 2)
- if len(keyAndValue) != 2 {
- return nil, fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue)
- }
- params.Add(keyAndValue[0], keyAndValue[1])
- }
-
- // Submit request
- resp, err := httpclient.DoRequest(ctx, "GET", resourceURL, params.Encode(), nil)
-
- if err != nil {
- return nil, fmt.Errorf("got error %s", err.Error())
- }
-
- return resp, nil
-}
diff --git a/cmd/login.go b/cmd/login.go
index 4dd30f4..34eee5f 100644
--- a/cmd/login.go
+++ b/cmd/login.go
@@ -12,7 +12,9 @@ import (
"github.com/elasticpath/epcc-cli/external/headergroups"
"github.com/elasticpath/epcc-cli/external/httpclient"
"github.com/elasticpath/epcc-cli/external/json"
+ "github.com/elasticpath/epcc-cli/external/oidc"
"github.com/elasticpath/epcc-cli/external/resources"
+ "github.com/elasticpath/epcc-cli/external/rest"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"net/url"
@@ -251,38 +253,7 @@ var loginImplicit = &cobra.Command{
},
RunE: func(cmd *cobra.Command, args []string) error {
-
- values := url.Values{}
- values.Set("grant_type", "implicit")
-
- env := config.GetEnv()
- if len(args) == 0 {
- log.Debug("Arguments have been passed, not using profile EPCC_CLIENT_ID")
- values.Set("client_id", env.EPCC_CLIENT_ID)
- }
-
- if len(args)%2 != 0 {
- return fmt.Errorf("invalid number of arguments supplied to login command, must be multiple of 2, not %v", len(args))
- }
-
- for i := 0; i < len(args); i += 2 {
- k := args[i]
- values.Set(k, args[i+1])
- }
-
- token, err := authentication.GetAuthenticationToken(false, &values, true)
-
- if err != nil {
- return err
- }
-
- if token != nil {
- log.Infof("Successfully authenticated with implicit token, session expires %s", time.Unix(token.Expires, 0).Format(time.RFC1123Z))
- } else {
- log.Warn("Did not successfully authenticate against the API")
- }
-
- return nil
+ return authentication.InternalImplicitAuthentication(args)
},
}
@@ -330,7 +301,7 @@ var loginCustomer = &cobra.Command{
newArgs = append(newArgs, "customer-token")
newArgs = append(newArgs, args...)
- body, err := createInternal(ctx, overrides, newArgs, false, "", false)
+ body, err := rest.CreateInternal(ctx, overrides, newArgs, false, "", false)
if err != nil {
log.Warnf("Login not completed successfully")
@@ -360,7 +331,7 @@ var loginCustomer = &cobra.Command{
if customerTokenResponse != nil {
// Get the customer so we have aliases where we need the id.
- getCustomerBody, err := getInternal(ctx, overrides, []string{"customer", customerTokenResponse.Data.CustomerId}, false)
+ getCustomerBody, err := rest.GetInternal(ctx, overrides, []string{"customer", customerTokenResponse.Data.CustomerId}, false)
if err != nil {
log.Warnf("Could not retrieve customer")
@@ -460,7 +431,7 @@ var loginAccountManagement = &cobra.Command{
}
// Populate an alias to get the authentication_realm.
- _, err := getInternal(ctx, overrides, []string{"account-authentication-settings"}, false)
+ _, err := rest.GetInternal(ctx, overrides, []string{"account-authentication-settings"}, false)
if err != nil {
return fmt.Errorf("couldn't determine authentication realm: %w", err)
@@ -485,7 +456,7 @@ var loginAccountManagement = &cobra.Command{
// Try and auto-detect the password profile id
if passwordAuthentication {
- resp, err := getInternal(ctx, overrides, []string{"password-profiles", "related_authentication_realm_for_account_authentication_settings_last_read=entity"}, false)
+ resp, err := rest.GetInternal(ctx, overrides, []string{"password-profiles", "related_authentication_realm_for_account_authentication_settings_last_read=entity"}, false)
if err != nil {
return fmt.Errorf("couldn't determine password profile: %w", err)
@@ -540,7 +511,7 @@ var loginAccountManagement = &cobra.Command{
}
// Do the login and get back a list of accounts
- body, err := createInternal(ctx, overrides, loginArgs, false, "", false)
+ body, err := rest.CreateInternal(ctx, overrides, loginArgs, false, "", false)
if err != nil {
log.Warnf("Login not completed successfully")
@@ -600,7 +571,7 @@ var loginAccountManagement = &cobra.Command{
authentication.SaveAccountManagementAuthenticationToken(*selectedAccount)
- accountMembers, err := getInternal(ctx, overrides, []string{"account-members"}, false)
+ accountMembers, err := rest.GetInternal(ctx, overrides, []string{"account-members"}, false)
if err == nil {
accountMemberId, _ := json.RunJQOnString(".data[0].id", accountMembers)
@@ -612,3 +583,13 @@ var loginAccountManagement = &cobra.Command{
return json.PrintJson(string(jsonBody))
},
}
+
+var OidcPort uint16 = 8080
+var loginOidc = &cobra.Command{
+ Use: "oidc",
+ Short: "Starts a local webserver to facilitate OIDC login flows",
+ Args: cobra.NoArgs,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return oidc.StartOIDCServer(OidcPort)
+ },
+}
diff --git a/cmd/reset-store.go b/cmd/reset-store.go
index 1cfcecf..cac506e 100644
--- a/cmd/reset-store.go
+++ b/cmd/reset-store.go
@@ -9,6 +9,7 @@ import (
"github.com/elasticpath/epcc-cli/external/httpclient"
"github.com/elasticpath/epcc-cli/external/json"
"github.com/elasticpath/epcc-cli/external/resources"
+ "github.com/elasticpath/epcc-cli/external/rest"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"io"
@@ -70,25 +71,25 @@ var ResetStore = &cobra.Command{
// We would also need locking to go faster.
// Get customer and account authentication settings to populate the aliases
- _, err = getInternal(ctx, overrides, []string{"customer-authentication-settings"}, false)
+ _, err = rest.GetInternal(ctx, overrides, []string{"customer-authentication-settings"}, false)
if err != nil {
errors = append(errors, err.Error())
}
- _, err = getInternal(ctx, overrides, []string{"account-authentication-settings"}, false)
+ _, err = rest.GetInternal(ctx, overrides, []string{"account-authentication-settings"}, false)
if err != nil {
errors = append(errors, err.Error())
}
- _, err = getInternal(ctx, overrides, []string{"merchant-realm-mappings"}, false)
+ _, err = rest.GetInternal(ctx, overrides, []string{"merchant-realm-mappings"}, false)
if err != nil {
errors = append(errors, err.Error())
}
- _, err = getInternal(ctx, overrides, []string{"authentication-realms"}, false)
+ _, err = rest.GetInternal(ctx, overrides, []string{"authentication-realms"}, false)
if err != nil {
errors = append(errors, err.Error())
@@ -200,7 +201,7 @@ func resetResourcesUndeletableResources(ctx context.Context, overrides *httpclie
errors := make([]string, 0)
for _, resetCmd := range resetCmds {
- body, err := updateInternal(ctx, overrides, false, resetCmd)
+ body, err := rest.UpdateInternal(ctx, overrides, false, resetCmd)
if err != nil {
errors = append(errors, fmt.Errorf("error resetting %s: %v", resetCmd[0], err).Error())
diff --git a/cmd/root.go b/cmd/root.go
index e029948..9ce3fba 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -144,7 +144,9 @@ func InitializeCmd() {
LoginCmd.AddCommand(loginDocs)
LoginCmd.AddCommand(loginCustomer)
LoginCmd.AddCommand(loginAccountManagement)
+ LoginCmd.AddCommand(loginOidc)
+ loginOidc.PersistentFlags().Uint16VarP(&OidcPort, "port", "p", 8080, "The port to listen on for the OIDC callback")
logoutCmd.AddCommand(logoutBearer)
logoutCmd.AddCommand(logoutCustomer)
logoutCmd.AddCommand(logoutAccountManagement)
diff --git a/cmd/update.go b/cmd/update.go
index f09e6ca..e617983 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -9,12 +9,9 @@ import (
"github.com/elasticpath/epcc-cli/external/httpclient"
"github.com/elasticpath/epcc-cli/external/json"
"github.com/elasticpath/epcc-cli/external/resources"
- "github.com/elasticpath/epcc-cli/external/shutdown"
+ "github.com/elasticpath/epcc-cli/external/rest"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
- "io"
- "net/url"
- "strings"
)
func NewUpdateCommand(parentCmd *cobra.Command) func() {
@@ -89,13 +86,13 @@ func NewUpdateCommand(parentCmd *cobra.Command) func() {
aliasId := aliases.ResolveAliasValuesOrReturnIdentity(resource.JsonApiType, resource.AlternateJsonApiTypesForAliases, ifAliasDoesNotExist, "id")
if aliasId != ifAliasDoesNotExist {
- // If the aliasId is different than the request then it does exist.
+ // If the aliasId is different from the request then it does exist.
log.Infof("Alias [%s] does exist (value: %s), not continuing run", ifAliasDoesNotExist, aliasId)
return nil
}
}
- body, err := updateInternal(context.Background(), overrides, skipAliases, append([]string{resourceName}, args...))
+ body, err := rest.UpdateInternal(context.Background(), overrides, skipAliases, append([]string{resourceName}, args...))
if err != nil {
return err
@@ -213,88 +210,3 @@ func NewUpdateCommand(parentCmd *cobra.Command) func() {
return resetFunc
}
-
-func updateInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, skipAliases bool, args []string) (string, error) {
- shutdown.OutstandingOpCounter.Add(1)
- defer shutdown.OutstandingOpCounter.Done()
-
- // Find Resource
- resource, ok := resources.GetResourceByName(args[0])
- if !ok {
- return "", fmt.Errorf("could not find resource %s", args[0])
- }
-
- if resource.UpdateEntityInfo == nil {
- return "", fmt.Errorf("resource %s doesn't support UPDATE", args[0])
- }
-
- // Count ids in UpdateEntity
- resourceUrlInfo := resource.UpdateEntityInfo
- idCount, err := resources.GetNumberOfVariablesNeeded(resourceUrlInfo.Url)
- if err != nil {
- return "", err
- }
-
- // Replace ids with args in resourceURL
- resourceURL, err := resources.GenerateUrl(resourceUrlInfo, args[1:], true)
- if err != nil {
- return "", err
- }
-
- if overrides.OverrideUrlPath != "" {
- log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath)
- resourceURL = overrides.OverrideUrlPath
- }
-
- args = append(args, "type", resource.JsonApiType)
- // Create the body from remaining args
- body, err := json.ToJson(args[(idCount+1):], resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true)
- if err != nil {
- return "", err
- }
-
- params := url.Values{}
-
- for _, v := range overrides.QueryParameters {
- keyAndValue := strings.SplitN(v, "=", 2)
- if len(keyAndValue) != 2 {
- return "", fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue)
- }
- params.Add(keyAndValue[0], keyAndValue[1])
- }
-
- // Submit request
- resp, err := httpclient.DoRequest(ctx, "PUT", resourceURL, params.Encode(), strings.NewReader(body))
- if err != nil {
- return "", fmt.Errorf("got error %s", err.Error())
- } else if resp == nil {
- return "", fmt.Errorf("got nil response")
- }
-
- if resp.Body != nil {
- defer resp.Body.Close()
-
- // Print the body
- resBody, err := io.ReadAll(resp.Body)
- if err != nil {
- log.Fatal(err)
- }
-
- // Check if error response
- if resp.StatusCode >= 400 && resp.StatusCode <= 600 {
- json.PrintJson(string(resBody))
- return "", fmt.Errorf(resp.Status)
- }
-
- // 204 is no content, so we will skip it.
- if resp.StatusCode != 204 {
- if !skipAliases {
- aliases.SaveAliasesForResources(string(resBody))
- }
- }
-
- return string(resBody), nil
- } else {
- return "", nil
- }
-}
diff --git a/external/authentication/internal_login.go b/external/authentication/internal_login.go
new file mode 100644
index 0000000..7ea805e
--- /dev/null
+++ b/external/authentication/internal_login.go
@@ -0,0 +1,43 @@
+package authentication
+
+import (
+ "fmt"
+ "github.com/elasticpath/epcc-cli/config"
+ log "github.com/sirupsen/logrus"
+ "net/url"
+ "time"
+)
+
+func InternalImplicitAuthentication(args []string) error {
+ values := url.Values{}
+ values.Set("grant_type", "implicit")
+
+ env := config.GetEnv()
+ if len(args) == 0 {
+ log.Debug("Arguments have been passed, not using profile EPCC_CLIENT_ID")
+ values.Set("client_id", env.EPCC_CLIENT_ID)
+ }
+
+ if len(args)%2 != 0 {
+ return fmt.Errorf("invalid number of arguments supplied to login command, must be multiple of 2, not %v", len(args))
+ }
+
+ for i := 0; i < len(args); i += 2 {
+ k := args[i]
+ values.Set(k, args[i+1])
+ }
+
+ token, err := GetAuthenticationToken(false, &values, true)
+
+ if err != nil {
+ return err
+ }
+
+ if token != nil {
+ log.Infof("Successfully authenticated with implicit token, session expires %s", time.Unix(token.Expires, 0).Format(time.RFC1123Z))
+ } else {
+ log.Warn("Did not successfully authenticate against the API")
+ }
+
+ return nil
+}
diff --git a/external/json/to_json.go b/external/json/to_json.go
index 41422bf..cf28fcb 100644
--- a/external/json/to_json.go
+++ b/external/json/to_json.go
@@ -7,6 +7,7 @@ import (
"github.com/elasticpath/epcc-cli/external/resources"
"github.com/elasticpath/epcc-cli/external/templates"
"github.com/itchyny/gojq"
+ "github.com/mitchellh/mapstructure"
log "github.com/sirupsen/logrus"
"regexp"
"strings"
@@ -192,6 +193,30 @@ func RunJQOnString(queryStr string, json string) (interface{}, error) {
return RunJQ(queryStr, obj)
}
+func RunJQOnStringAndGetString(queryStr string, json string) (string, error) {
+ result, err := RunJQOnString(queryStr, json)
+
+ if err != nil {
+ return "", err
+ }
+
+ if result, ok := result.(string); ok {
+ return result, nil
+ }
+
+ return "", fmt.Errorf("could not convert %T into string", result)
+}
+
+func RunJQOnStringAndMarshalResponse(queryStr string, json string, obj any) error {
+ result, err := RunJQOnString(queryStr, json)
+
+ if err != nil {
+ return err
+ }
+
+ return mapstructure.Decode(result, &obj)
+}
+
func RunJQ(queryStr string, result interface{}) (interface{}, error) {
query, err := gojq.Parse(queryStr)
diff --git a/external/oidc/callback.go b/external/oidc/callback.go
new file mode 100644
index 0000000..d0d9bdc
--- /dev/null
+++ b/external/oidc/callback.go
@@ -0,0 +1,138 @@
+package oidc
+
+import (
+ "context"
+ "encoding/base64"
+ gojson "encoding/json"
+ "fmt"
+ "github.com/elasticpath/epcc-cli/external/authentication"
+ "github.com/elasticpath/epcc-cli/external/httpclient"
+ "github.com/elasticpath/epcc-cli/external/rest"
+ "net/http"
+)
+
+type CallbackPageInfo struct {
+ LoginType string
+ ErrorTitle string
+ ErrorDescription string
+ AccountTokenResponse *authentication.AccountManagementAuthenticationTokenResponse
+ CustomerTokenResponse *authentication.CustomerTokenResponse
+ AccountTokenStructBase64 []string
+ CustomerTokenStructBase64 string
+}
+
+func GetCallbackData(ctx context.Context, port uint16, r *http.Request) (*CallbackPageInfo, error) {
+ // Parse the query parameters
+ queryParams := r.URL.Query()
+
+ // Convert query parameters to a map
+ data := make(map[string]string)
+ for key, values := range queryParams {
+ data[key] = values[0] // Use the first value if multiple are provided
+ }
+
+ data["uri"] = fmt.Sprintf("http://localhost:%d", port)
+
+ if data["code"] != "" {
+
+ state, err := r.Cookie("state")
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get state cookie: %w", err)
+ }
+
+ verifier, err := r.Cookie("code_verifier")
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get verifier cookie: %w", err)
+ }
+
+ login_type, err := r.Cookie("login_type")
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get login_type cookie: %w", err)
+ }
+
+ if data["state"] != state.Value {
+ return &CallbackPageInfo{
+ ErrorTitle: "State Mismatch",
+ ErrorDescription: "State mismatch between locally stored value and value from IdP",
+ LoginType: login_type.Value,
+ }, nil
+ }
+
+ cpi := CallbackPageInfo{
+ LoginType: login_type.Value,
+ }
+
+ if login_type.Value == "AM" {
+ result, err := rest.CreateInternal(context.Background(), &httpclient.HttpParameterOverrides{}, []string{"account-management-authentication-token",
+ "authentication_mechanism", "oidc",
+ "oauth_authorization_code", data["code"],
+ "oauth_redirect_uri", fmt.Sprintf("http://localhost:%d/callback", port),
+ "oauth_code_verifier", verifier.Value,
+ }, false, "", true)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get account tokens: %w", err)
+ }
+
+ err = gojson.Unmarshal([]byte(result), &cpi.AccountTokenResponse)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not unmarshal response: %w", err)
+ }
+
+ for _, v := range cpi.AccountTokenResponse.Data {
+
+ str, err := gojson.Marshal(v)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not encode token: %w", err)
+ }
+
+ cpi.AccountTokenStructBase64 = append(cpi.AccountTokenStructBase64, base64.URLEncoding.EncodeToString(str))
+ }
+
+ return &cpi, nil
+ } else if login_type.Value == "Customers" {
+ result, err := rest.CreateInternal(context.Background(), &httpclient.HttpParameterOverrides{}, []string{"customer-token",
+ "authentication_mechanism", "oidc",
+ "oauth_authorization_code", data["code"],
+ "oauth_redirect_uri", fmt.Sprintf("http://localhost:%d/callback", port),
+ "oauth_code_verifier", verifier.Value,
+ }, false, "", true)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get customer tokens: %w", err)
+ }
+
+ err = gojson.Unmarshal([]byte(result), &cpi.CustomerTokenResponse)
+
+ str, err := gojson.Marshal(cpi.CustomerTokenResponse.Data)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not encode token: %w", err)
+ }
+
+ cpi.CustomerTokenStructBase64 = base64.URLEncoding.EncodeToString(str)
+ return &cpi, nil
+ } else {
+ return &CallbackPageInfo{
+ ErrorTitle: "Unknown Login Type",
+ ErrorDescription: fmt.Sprintf("Unsupported login type used: %v", login_type.Value),
+ }, nil
+ }
+ } else if data["error"] == "" {
+
+ return &CallbackPageInfo{
+ ErrorTitle: "Bad Response",
+ ErrorDescription: "Invalid response from IdP, no code or error query parameter",
+ }, nil
+ } else {
+ return &CallbackPageInfo{
+ ErrorTitle: data["error"],
+ ErrorDescription: data["error_description"],
+ }, nil
+ }
+}
diff --git a/external/oidc/get_token.go b/external/oidc/get_token.go
new file mode 100644
index 0000000..f91a085
--- /dev/null
+++ b/external/oidc/get_token.go
@@ -0,0 +1,155 @@
+package oidc
+
+import (
+ "context"
+ "encoding/base64"
+ gojson "encoding/json"
+ "fmt"
+ "github.com/elasticpath/epcc-cli/external/authentication"
+ "github.com/elasticpath/epcc-cli/external/httpclient"
+ "github.com/elasticpath/epcc-cli/external/json"
+ "github.com/elasticpath/epcc-cli/external/rest"
+ log "github.com/sirupsen/logrus"
+ "net/http"
+ "os"
+ "time"
+)
+
+type TokenPageInfo struct {
+ LoginType string
+ ErrorTitle string
+ ErrorDescription string
+ Name string
+ Id string
+}
+
+func GetTokenData(ctx context.Context, port uint16, r *http.Request) (*TokenPageInfo, error) {
+ // Parse the query parameters
+ queryParams := r.URL.Query()
+
+ // Convert query parameters to a map
+ data := make(map[string]string)
+ for key, values := range queryParams {
+ data[key] = values[0] // Use the first value if multiple are provided
+ }
+
+ if data["login_type"] == "AM" {
+ token := data["token"]
+ amTokenJson, err := base64.URLEncoding.DecodeString(token)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get decode am token: %w", err)
+ }
+
+ amToken := authentication.AccountManagementAuthenticationTokenStruct{}
+
+ err = gojson.Unmarshal(amTokenJson, &amToken)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get unmarshal am token: %w", err)
+ }
+
+ authentication.SaveAccountManagementAuthenticationToken(amToken)
+
+ apiToken := authentication.GetApiToken()
+
+ if apiToken != nil {
+ if apiToken.Identifier == "client_credentials" {
+ log.Warnf("You are currently logged in with client_credentials, please switch to implicit with `epcc login implicit` to use the account management token correctly. Mixing client_credentials and the account management token can lead to unintended results.")
+ }
+ }
+
+ go func() {
+ time.Sleep(2 * time.Second)
+ log.Infof("Authentication complete, shutting down")
+ os.Exit(0)
+ }()
+
+ return &TokenPageInfo{
+ LoginType: "AM",
+ Name: amToken.AccountName,
+ Id: amToken.AccountId,
+ }, nil
+
+ } else if data["login_type"] == "Customers" {
+ token := data["token"]
+ custToken, err := base64.URLEncoding.DecodeString(token)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get decode am token: %w", err)
+ }
+
+ custTokenStruct := authentication.CustomerTokenStruct{}
+
+ err = gojson.Unmarshal(custToken, &custTokenStruct)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get unmarshal am token: %w", err)
+ }
+
+ ctr := authentication.CustomerTokenResponse{
+ Data: custTokenStruct,
+ AdditionalInfo: authentication.CustomerTokenEpccCliAdditionalInfo{},
+ }
+
+ authentication.SaveCustomerToken(ctr)
+
+ apiToken := authentication.GetApiToken()
+
+ if apiToken != nil {
+ if apiToken.Identifier == "client_credentials" {
+ log.Warnf("You are currently logged in with client_credentials, please switch to implicit with `epcc login implicit` to use the customer token correctly. Mixing client_credentials and the customer token can lead to unintended results.")
+ }
+ }
+
+ if authentication.IsAccountManagementAuthenticationTokenSet() {
+ log.Warnf("Logging out of Account Management")
+ authentication.ClearAccountManagementAuthenticationToken()
+ }
+
+ result, err := rest.GetInternal(context.Background(), &httpclient.HttpParameterOverrides{}, []string{"customer", custTokenStruct.CustomerId}, false)
+
+ customerName := "Unknown"
+ customerEmail := "Unkwown"
+
+ if err == nil {
+ customerName, err = json.RunJQOnStringAndGetString(".data.name", result)
+
+ if err != nil {
+ log.Warnf("Could not get customer name from response %s, %v", result, err)
+ }
+
+ customerEmail, err = json.RunJQOnStringAndGetString(".data.email", result)
+
+ if err != nil {
+ log.Warnf("Could not get customer email from response %s, %v", result, err)
+ }
+
+ ctr := authentication.CustomerTokenResponse{
+ Data: custTokenStruct,
+ AdditionalInfo: authentication.CustomerTokenEpccCliAdditionalInfo{
+ CustomerName: customerName,
+ CustomerEmail: customerEmail,
+ },
+ }
+
+ log.Infof("Saving customer token with %s,%s, %v", customerName, customerEmail, result)
+ authentication.SaveCustomerToken(ctr)
+ }
+
+ go func() {
+ time.Sleep(2 * time.Second)
+ log.Infof("Authentication complete, shutting down")
+ os.Exit(0)
+ }()
+
+ return &TokenPageInfo{
+ LoginType: "Customers",
+ Name: customerName,
+ Id: custTokenStruct.CustomerId,
+ }, nil
+
+ }
+
+ return nil, fmt.Errorf("invalid login type")
+}
diff --git a/external/oidc/index.go b/external/oidc/index.go
new file mode 100644
index 0000000..4551f21
--- /dev/null
+++ b/external/oidc/index.go
@@ -0,0 +1,160 @@
+package oidc
+
+import (
+ "context"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "github.com/elasticpath/epcc-cli/external/httpclient"
+ "github.com/elasticpath/epcc-cli/external/json"
+ "github.com/elasticpath/epcc-cli/external/rest"
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ log "github.com/sirupsen/logrus"
+)
+
+type LoginPageInfo struct {
+ CustomerProfiles []OidcProfileInfo
+ CustomerClientId string
+ AccountProfiles []OidcProfileInfo
+ AccountClientId string
+ RedirectUriUnencoded string
+ RedirectUriEncoded string
+ State string
+ CodeVerifier string
+ CodeChallenge string
+}
+
+func GetIndexData(ctx context.Context, port uint16) (*LoginPageInfo, error) {
+
+ overrides := &httpclient.HttpParameterOverrides{
+ QueryParameters: nil,
+ OverrideUrlPath: "",
+ }
+
+ // Get customer and account authentication settings to populate the aliases
+ customerAuthSettings, err := rest.GetInternal(ctx, overrides, []string{"customer-authentication-settings"}, false)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not retrieve customer authentication settings: %w", err)
+ }
+
+ accountAuthSettings, err := rest.GetInternal(ctx, overrides, []string{"account-authentication-settings"}, false)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not retrieve account authentication settings: %w", err)
+ }
+
+ customerRealmId, err := json.RunJQOnStringAndGetString(".data.relationships[\"authentication-realm\"].data.id", customerAuthSettings)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not determine customer realm id: %w", err)
+ }
+
+ customerClientId, err := json.RunJQOnStringAndGetString(".data.meta.client_id", customerAuthSettings)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not determine customer client id: %w", err)
+ }
+
+ accountRealmId, err := json.RunJQOnStringAndGetString(".data.relationships.authentication_realm.data.id", accountAuthSettings)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not determine account realm id: %w", err)
+ }
+
+ accountClientId, err := json.RunJQOnStringAndGetString(".data.meta.client_id", accountAuthSettings)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not determine account client id: %w", err)
+ }
+
+ customerProfiles, err := getOidcProfilesForRealm(ctx, overrides, customerRealmId)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get oidc profiles for customers: %w", err)
+ }
+
+ accountProfiles, err := getOidcProfilesForRealm(ctx, overrides, accountRealmId)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get oidc profiles for customers: %w", err)
+ }
+
+ verifier, err := GenerateCodeVerifier()
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get code verifier: %w", err)
+ }
+
+ challenge, err := GenerateCodeChallenge(verifier)
+
+ if err != nil {
+ return nil, fmt.Errorf("could not get code challenge: %w", err)
+ }
+
+ profiles := LoginPageInfo{
+ CustomerProfiles: customerProfiles,
+ CustomerClientId: customerClientId,
+ AccountProfiles: accountProfiles,
+ AccountClientId: accountClientId,
+ State: uuid.New().String(),
+ RedirectUriEncoded: fmt.Sprintf("%s%d%s", "http%3A%2F%2Flocalhost%3A", port, "/callback"),
+ RedirectUriUnencoded: fmt.Sprintf("%s%d%s", "http://localhost:", port, "/callback"),
+ CodeVerifier: verifier,
+ CodeChallenge: challenge,
+ }
+
+ return &profiles, nil
+}
+
+func getOidcProfilesForRealm(ctx context.Context, overrides *httpclient.HttpParameterOverrides, realmId string) ([]OidcProfileInfo, error) {
+ res, err := rest.GetInternal(ctx, overrides, []string{"oidc-profiles", realmId}, false)
+
+ if err != nil {
+ return nil, err
+ }
+
+ resObj, err := json.RunJQOnString(".data | map({name: .name, authorization_link: .links[\"authorization-endpoint\"], idp: .meta.issuer})", res)
+
+ if err != nil {
+ log.Errorf("Couldn't get oidc profile information: %v", err)
+ return nil, err
+ }
+
+ var result []OidcProfileInfo
+
+ err = mapstructure.Decode(resObj, &result)
+
+ if err != nil {
+ log.Errorf("Couldn't convert into map: %v", err)
+ return nil, err
+ }
+
+ return result, nil
+
+}
+
+// GenerateCodeVerifier generates a securely random code verifier.
+func GenerateCodeVerifier() (string, error) {
+ // PKCE requires a code verifier between 43 and 128 characters.
+ // We'll generate 32 bytes of randomness, which Base64 encodes to 43 characters.
+ verifier := make([]byte, 32)
+ _, err := rand.Read(verifier)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate random verifier: %w", err)
+ }
+
+ // Base64 URL encode the random bytes
+ return base64.RawURLEncoding.EncodeToString(verifier), nil
+}
+
+// GenerateCodeChallenge generates the S256 code challenge from the verifier.
+func GenerateCodeChallenge(verifier string) (string, error) {
+ // Compute the SHA256 hash of the verifier
+ hash := sha256.Sum256([]byte(verifier))
+
+ // Base64 URL encode the hash
+ return base64.RawURLEncoding.EncodeToString(hash[:]), nil
+}
diff --git a/external/oidc/server.go b/external/oidc/server.go
new file mode 100644
index 0000000..785d5fa
--- /dev/null
+++ b/external/oidc/server.go
@@ -0,0 +1,114 @@
+package oidc
+
+import (
+ "bytes"
+ "context"
+ "embed"
+ "fmt"
+ "github.com/elasticpath/epcc-cli/external/browser"
+ "path/filepath"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "net/http"
+ "strings"
+ "text/template"
+)
+
+//go:embed site/*
+var EmbedFS embed.FS
+
+func StartOIDCServer(port uint16) error {
+
+ // Parse all templates at startup
+ var err error
+ templates, err := template.ParseFS(EmbedFS, "site/*.gohtml")
+ if err != nil {
+ panic(fmt.Sprintf("Error parsing templates: %v", err))
+ }
+
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+
+ ctx := context.Background()
+ log.Tracef("Handling request %s %s", r.Method, r.URL.Path)
+
+ // Remove leading "/" and sanitize the path
+ requestPath := strings.TrimPrefix(r.URL.Path, "/")
+ if requestPath == "" {
+ requestPath = "index"
+ }
+
+ // Check if it's a .html request for template rendering
+ if filepath.Ext(requestPath) == "" {
+
+ templateName := requestPath + ".gohtml"
+
+ var data any
+ var error error
+
+ switch requestPath {
+ case "index":
+ data, error = GetIndexData(ctx, port)
+ case "callback":
+ data, error = GetCallbackData(ctx, port, r)
+ case "get_token":
+ data, error = GetTokenData(ctx, port, r)
+ }
+
+ if error != nil {
+ log.Errorf("Request %s %s => %s", r.Method, r.URL.Path, "500")
+ http.Error(w, fmt.Sprintf("Error getting data: %v", error), http.StatusInternalServerError)
+ return
+ }
+
+ // Render the template by its base name
+ if tmpl := templates.Lookup(templateName); tmpl != nil {
+ if err := tmpl.Execute(w, data); err != nil {
+ log.Errorf("Request %s %s => %s", r.Method, r.URL.Path, "500")
+ http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError)
+ }
+
+ log.Warnf("Request %s %s => %s", r.Method, r.URL.Path, "200")
+ return
+ }
+
+ log.Infof("Request %s %s => %s", r.Method, r.URL.Path, "404")
+ http.NotFound(w, r)
+ return
+ }
+
+ file, err := EmbedFS.ReadFile("site/" + requestPath)
+ if err != nil {
+ log.Warnf("Request %s %s => %s", r.Method, r.URL.Path, "404")
+ http.NotFound(w, r)
+ return
+ }
+
+ if filepath.Ext(requestPath) == ".css" {
+ w.Header().Set("Content-Type", "text/css")
+ }
+
+ http.ServeContent(w, r, requestPath, time.Now(), bytes.NewReader(file))
+ log.Infof("Request %s %s => %s", r.Method, r.URL.Path, "200")
+ })
+
+ log.Infof("Starting server on port %d", port)
+
+ go func() {
+ log.Warnf("Waiting for server to start")
+ time.Sleep(1 * time.Second)
+ browser.OpenUrl(fmt.Sprintf("http://localhost:%d", port))
+
+ }()
+
+ err = http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
+
+ return err
+
+}
+
+type OidcProfileInfo struct {
+ Name string `mapstructure:"name"`
+ AuthorizationLink string `mapstructure:"authorization_link"`
+ Idp string
+}
diff --git a/external/oidc/site/callback.gohtml b/external/oidc/site/callback.gohtml
new file mode 100644
index 0000000..a698642
--- /dev/null
+++ b/external/oidc/site/callback.gohtml
@@ -0,0 +1,52 @@
+{{ template "header" }}
+ {{ if ne .ErrorTitle "" }}
+
errorAn Error has Occurred
+
Error Type: {{ .ErrorTitle }}
+
Error Description: {{ .ErrorDescription }}
+ {{ else if eq .LoginType "AM" }}
+
check_circle Authentication With Identity Provider Successful
+
+ To finish the authentication process, you must select an account. If you see no accounts make sure that auto_create_account_for_account_members is enabled or manually create an account.
+
+
+ epcc update account-authentication-setting auto_create_account_for_account_members true
+
+
+
Please Select An Account
+
+
+
Account Name
+
Account ID
+
Action
+
+
+ {{ range $i, $item := .AccountTokenResponse.Data }}
+
+
{{ $item.AccountName }}
+
{{ $item.AccountId }}
+
+
+ {{ end }}
+ {{ else if eq .LoginType "Customers" }}
+
check_circle Authentication With Identity Provider Successful
+ To continue, press GO next to the Customer.
+
+
+
Customer ID
+
Action
+
+
+
+
{{ .CustomerTokenResponse.Data.Id }}
+
+
+
+ {{ end }}
+
+
+
+
+
+
+ To return to start Click here.
+{{ template "footer" }}
\ No newline at end of file
diff --git a/external/oidc/site/favicon.ico b/external/oidc/site/favicon.ico
new file mode 100644
index 0000000..78d8c64
Binary files /dev/null and b/external/oidc/site/favicon.ico differ
diff --git a/external/oidc/site/footer.gohtml b/external/oidc/site/footer.gohtml
new file mode 100644
index 0000000..4c82922
--- /dev/null
+++ b/external/oidc/site/footer.gohtml
@@ -0,0 +1,5 @@
+{{ define "footer" }}
+
+