From 1b3ef0605d259d6dab7f4ba0242969a12ed8276e Mon Sep 17 00:00:00 2001 From: Sandro Manke Date: Sun, 29 Dec 2019 11:31:41 +0100 Subject: [PATCH] Okta Challenge Preferences As I had issues with Clisso not selecting push notifications but require me to enter codes instead of using push, even though I only had okta push turned on, I added this functionality, so we prefer push when possible but make it configurable for whoever is into doing it. Worked well here so far in different scenarios. --- README.md | 10 ++++++++++ cmd/providers.go | 11 +++++++---- config/config.go | 8 +++++--- okta/client.go | 20 +++++++++++--------- okta/get.go | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0852556..86c11cd 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,16 @@ The `--url` flag is the app's **embed link**. This can be retrieved as an Okta u the URL of an app on the Okta web UI. The same can also be retrieved as an administrator by clicking an app in the **Applications** view. The embed link is on the **General** tab. +The `--challenges` flag gives you the option to select which challenges in what order to use. +This flag is optional. +Currently only 2 challenge types are supported: + +- push +- token:software:totp + +Clisso will use the challenges in the preference you specified. +You can make a list by specifiying the flag multiple times like `--challenges push --challenges token:software:totp` + >NOTE: An Okta embed link must not contain an HTTP query, only the base URL. For AWS apps, the link should end with `/137`. diff --git a/cmd/providers.go b/cmd/providers.go index 2d96fd4..e329bc6 100644 --- a/cmd/providers.go +++ b/cmd/providers.go @@ -23,6 +23,7 @@ var providerDuration int // Okta var baseURL string +var challenges []string func init() { // OneLogin @@ -43,6 +44,7 @@ func init() { // Okta cmdProvidersCreateOkta.Flags().StringVar(&baseURL, "base-url", "", "Okta base URL") + cmdProvidersCreateOkta.Flags().StringArrayVar(&challenges, "challenges", []string{"push", "token:software:totp"}, "Challenges to be used with preference") cmdProvidersCreateOkta.Flags().StringVar(&username, "username", "", "Don't ask for a username and use this instead") cmdProvidersCreateOkta.Flags().IntVar(&providerDuration, "duration", 0, "(Optional) Default session duration in seconds") @@ -176,10 +178,11 @@ var cmdProvidersCreateOkta = &cobra.Command{ log.Fatalf(color.RedString("Provider '%s' already exists"), name) } - conf := map[string]string{ - "base-url": baseURL, - "type": "okta", - "username": username, + conf := map[string]interface{}{ + "base-url": baseURL, + "type": "okta", + "username": username, + "challenges": challenges, } if providerDuration != 0 { // Duration specified - validate value diff --git a/config/config.go b/config/config.go index 9654518..8c51c48 100644 --- a/config/config.go +++ b/config/config.go @@ -77,20 +77,22 @@ func GetOneLoginApp(app string) (*OneLoginAppConfig, error) { // OktaProviderConfig represents an Okta provider configuration. type OktaProviderConfig struct { - BaseURL string - Username string + BaseURL string + Username string + Challenges []string } // GetOktaProvider returns a OktaProviderConfig struct containing the configuration for provider p. func GetOktaProvider(p string) (*OktaProviderConfig, error) { baseURL := viper.GetString(fmt.Sprintf("providers.%s.base-url", p)) username := viper.GetString(fmt.Sprintf("providers.%s.username", p)) + challenges := viper.GetStringSlice(fmt.Sprintf("providers.%s.challenges", p)) if baseURL == "" { return nil, errors.New("base-url config value must bet set") } - return &OktaProviderConfig{BaseURL: baseURL, Username: username}, nil + return &OktaProviderConfig{BaseURL: baseURL, Username: username, Challenges: challenges}, nil } // OktaAppConfig represents an Okta app configuration. diff --git a/okta/client.go b/okta/client.go index 78a16ed..fbe135e 100644 --- a/okta/client.go +++ b/okta/client.go @@ -38,18 +38,20 @@ type GetSessionTokenResponse struct { StateToken string `json:"stateToken"` Status string `json:"status"` Embedded struct { - Factors []struct { - ID string `json:"id"` - Links struct { - Verify struct { - Href string `json:"href"` - } `json:"verify"` - } `json:"_links"` - FactorType string `json:"factorType"` - } `json:"factors"` + Factors []factor `json:"factors"` } `json:"_embedded"` } +type factor struct { + ID string `json:"id"` + Links struct { + Verify struct { + Href string `json:"href"` + } `json:"verify"` + } `json:"_links"` + FactorType string `json:"factorType"` +} + // GetSessionToken performs a login operation against the Okta API and returns a session token upon // successful login. // diff --git a/okta/get.go b/okta/get.go index 5e2db30..1b51620 100644 --- a/okta/get.go +++ b/okta/get.go @@ -3,6 +3,7 @@ package okta import ( "fmt" "log" + "strings" "time" "github.com/allcloud-io/clisso/aws" @@ -77,6 +78,15 @@ func Get(app, provider string, duration int64) (*aws.Credentials, error) { st = resp.SessionToken case StatusMFARequired: factor := resp.Embedded.Factors[0] + + if len(p.Challenges) > 0 { + // use preferred list if available + factor, err = preferredFactor(resp.Embedded.Factors, p.Challenges) + if err != nil { + return nil, err + } + } + stateToken := resp.StateToken var vfResp *VerifyFactorResponse @@ -163,3 +173,34 @@ func Get(app, provider string, duration int64) (*aws.Credentials, error) { return creds, err } + +func typeMapOf(factors []factor) (m map[string]factor) { + m = make(map[string]factor) + + for _, v := range factors { + m[v.FactorType] = v + } + return +} + +func keys(m map[string]factor) (keys []string) { + keys = make([]string, len(m)) + + i := 0 + for k := range m { + keys[i] = k + i++ + } + return +} + +func preferredFactor(factors []factor, pref []string) (factor, error) { + fmap := typeMapOf(factors) + for _, p := range pref { + if f, ok := fmap[p]; ok { + return f, nil + } + } + + return factor{}, fmt.Errorf("no supported MFA type found in: %v", strings.Join(keys(fmap), ",")) +}