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:
parent
364f8789cd
commit
24e8b73c73
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
mfa/okta: migrate to use official Okta SDK
|
||||
```
|
2
go.mod
2
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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":
|
||||
|
|
Loading…
Reference in New Issue