Updating Okta MFA to use official SDK (#15355)

* updating MFA to use official Okta SDK

* add changelog

* Update vault/login_mfa.go

Co-authored-by: swayne275 <swayne@hashicorp.com>

* cleanup query param building

* skip if not user factor

* updating struct tags to be more explicit

* fixing incorrect merge

* worrying that URL construction may change in the future, reimplementing GetFactorTransactionStatus

* adding some safety around url building

Co-authored-by: swayne275 <swayne@hashicorp.com>
This commit is contained in:
Chris Hoffman 2022-05-17 15:14:26 -04:00 committed by GitHub
parent 364f8789cd
commit 24e8b73c73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 57 deletions

3
changelog/15355.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
mfa/okta: migrate to use official Okta SDK
```

2
go.mod
View File

@ -144,7 +144,7 @@ require (
github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc
github.com/ncw/swift v1.0.47
github.com/oklog/run v1.1.0
github.com/okta/okta-sdk-golang/v2 v2.9.1
github.com/okta/okta-sdk-golang/v2 v2.12.1
github.com/oracle/oci-go-sdk v13.1.0+incompatible
github.com/ory/dockertest v3.3.5+incompatible
github.com/ory/dockertest/v3 v3.8.0

4
go.sum
View File

@ -1263,8 +1263,8 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/okta/okta-sdk-golang/v2 v2.9.1 h1:oiagkSEb54SZUbfVbX2rGMOqPPfKnCQJgT5R4qQPKHI=
github.com/okta/okta-sdk-golang/v2 v2.9.1/go.mod h1:0y8stgdplWMjaEbMr4mVtw0R+BdktpGZRw2sWKZWsMs=
github.com/okta/okta-sdk-golang/v2 v2.12.1 h1:U+smE7trkHSZO8Mval3Ow85dbxawO+pMAr692VZq9gM=
github.com/okta/okta-sdk-golang/v2 v2.12.1/go.mod h1:KRoAArk1H216oiRnQT77UN6JAhBOnOWkK27yA1SM7FQ=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

View File

@ -14,7 +14,6 @@ import (
"sync"
"time"
"github.com/chrismalek/oktasdk-go/okta"
duoapi "github.com/duosecurity/duo_api_golang"
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/golang-jwt/jwt/v4"
@ -36,6 +35,8 @@ import (
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault/quotas"
"github.com/mitchellh/mapstructure"
"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/okta/okta-sdk-golang/v2/okta/query"
"github.com/patrickmn/go-cache"
otplib "github.com/pquerna/otp"
totplib "github.com/pquerna/otp/totp"
@ -1863,31 +1864,33 @@ func (c *Core) validateOkta(ctx context.Context, mConfig *mfa.Config, username s
return fmt.Errorf("failed to get Okta configuration for method %q", mConfig.Name)
}
var client *okta.Client
if oktaConfig.BaseURL != "" {
var err error
client, err = okta.NewClientWithDomain(cleanhttp.DefaultClient(), oktaConfig.OrgName, oktaConfig.BaseURL, oktaConfig.APIToken)
if err != nil {
return errwrap.Wrapf("error getting Okta client: {{err}}", err)
}
} else {
client = okta.NewClient(cleanhttp.DefaultClient(), oktaConfig.OrgName, oktaConfig.APIToken, oktaConfig.Production)
baseURL := oktaConfig.BaseURL
if baseURL == "" {
baseURL = "okta.com"
}
orgURL, err := url.Parse(fmt.Sprintf("https://%s.%s", oktaConfig.OrgName, baseURL))
if err != nil {
return err
}
// Disable client side rate limiting
client.RateRemainingFloor = 0
var filterOpts *okta.UserListFilterOptions
ctx, client, err := okta.NewClient(ctx,
okta.WithToken(oktaConfig.APIToken),
okta.WithOrgUrl(orgURL.String()),
// Do not use cache or polling MFA will not refresh
okta.WithCache(false),
)
if err != nil {
return fmt.Errorf("error creating client: %s", err)
}
filterField := "profile.login"
if oktaConfig.PrimaryEmail {
filterOpts = &okta.UserListFilterOptions{
EmailEqualTo: username,
}
} else {
filterOpts = &okta.UserListFilterOptions{
LoginEqualTo: username,
}
filterField = "profile.email"
}
filterQuery := fmt.Sprintf("%s eq %q", filterField, username)
filter := query.NewQueryParams(query.WithFilter(filterQuery))
users, _, err := client.Users.ListWithFilter(filterOpts)
users, _, err := client.User.ListUsers(ctx, filter)
if err != nil {
return err
}
@ -1898,50 +1901,34 @@ func (c *Core) validateOkta(ctx context.Context, mConfig *mfa.Config, username s
return fmt.Errorf("more than one user found for e-mail address")
}
user := &users[0]
user := users[0]
_, err = client.Users.PopulateMFAFactors(user)
factors, _, err := client.UserFactor.ListFactors(ctx, user.Id)
if err != nil {
return err
}
if len(user.MFAFactors) == 0 {
if len(factors) == 0 {
return fmt.Errorf("no MFA factors found for user")
}
var factorID string
for _, factor := range user.MFAFactors {
if factor.FactorType == "push" {
factorID = factor.ID
break
var factorFound bool
var userFactor *okta.UserFactor
for _, factor := range factors {
if factor.IsUserFactorInstance() {
userFactor = factor.(*okta.UserFactor)
if userFactor.FactorType == "push" {
factorFound = true
break
}
}
}
if factorID == "" {
if !factorFound {
return fmt.Errorf("no push-type MFA factor found for user")
}
type pollInfo struct {
ValidationURL string `json:"href"`
}
type pushLinks struct {
Poll pollInfo `json:"poll"`
}
type pushResult struct {
Expiration time.Time `json:"expiresAt"`
FactorResult string `json:"factorResult"`
Links pushLinks `json:"_links"`
}
req, err := client.NewRequest("POST", fmt.Sprintf("users/%s/factors/%s/verify", user.ID, factorID), nil)
if err != nil {
return err
}
var result pushResult
_, err = client.Do(req, &result)
result, _, err := client.UserFactor.VerifyFactor(ctx, user.Id, userFactor.Id, okta.VerifyFactorRequest{}, userFactor, nil)
if err != nil {
return err
}
@ -1950,16 +1937,36 @@ func (c *Core) validateOkta(ctx context.Context, mConfig *mfa.Config, username s
return fmt.Errorf("expected WAITING status for push status, got %q", result.FactorResult)
}
// Parse links to get polling link
type linksObj struct {
Poll struct {
Href string `mapstructure:"href"`
} `mapstructure:"poll"`
}
links := new(linksObj)
if err := mapstructure.WeakDecode(result.Links, links); err != nil {
return err
}
// Strip the org URL from the fully qualified poll URL
url, err := url.Parse(strings.Replace(links.Poll.Href, orgURL.String(), "", 1))
if err != nil {
return err
}
for {
req, err := client.NewRequest("GET", result.Links.Poll.ValidationURL, nil)
// Okta provides an SDK method `GetFactorTransactionStatus` but does not provide the transaction id in
// the VerifyFactor respone. This code effectively reimplements that method.
rq := client.CloneRequestExecutor()
req, err := rq.WithAccept("application/json").WithContentType("application/json").NewRequest("GET", url.String(), nil)
if err != nil {
return err
}
var result pushResult
_, err = client.Do(req, &result)
var result *okta.VerifyUserFactorResponse
_, err = rq.Do(ctx, req, &result)
if err != nil {
return err
}
switch result.FactorResult {
case "WAITING":
case "SUCCESS":