Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auth/okta: Add support for Okta number challenge #15361

Merged
merged 9 commits into from
May 12, 2022
24 changes: 23 additions & 1 deletion builtin/credential/okta/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hashicorp/vault/sdk/helper/cidrutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/patrickmn/go-cache"
)

const (
Expand All @@ -34,6 +35,7 @@ func Backend() *backend {
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"login/*",
"verify/*",
},
SealWrapStorage: []string{
"config",
Expand All @@ -47,20 +49,23 @@ func Backend() *backend {
pathUsersList(&b),
pathGroupsList(&b),
pathLogin(&b),
pathVerify(&b),
},

AuthRenew: b.pathLoginRenew,
BackendType: logical.TypeCredential,
}
b.verifyCache = cache.New(5*time.Minute, time.Minute)

return &b
}

type backend struct {
*framework.Backend
verifyCache *cache.Cache
}

func (b *backend) Login(ctx context.Context, req *logical.Request, username, password, totp, preferredProvider string) ([]string, *logical.Response, []string, error) {
func (b *backend) Login(ctx context.Context, req *logical.Request, username, password, totp, nonce, preferredProvider string) ([]string, *logical.Response, []string, error) {
cfg, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, nil, nil, err
Expand Down Expand Up @@ -89,11 +94,17 @@ func (b *backend) Login(ctx context.Context, req *logical.Request, username, pas
Id string `json:"id"`
Type string `json:"factorType"`
Provider string `json:"provider"`
Embedded struct {
Challenge struct {
CorrectAnswer *int `json:"correctAnswer"`
} `json:"challenge"`
} `json:"_embedded"`
}

type embeddedResult struct {
User okta.User `json:"user"`
Factors []mfaFactor `json:"factors"`
Factor *mfaFactor `json:"factor"`
}

type authResult struct {
Expand Down Expand Up @@ -238,6 +249,17 @@ func (b *backend) Login(ctx context.Context, req *logical.Request, username, pas
return nil, logical.ErrorResponse(fmt.Sprintf("okta auth failed creating verify request: %v", err)), nil, nil
}
rsp, err := shim.Do(verifyReq, &result)

// Store number challenge if found
numberChallenge := result.Embedded.Factor.Embedded.Challenge.CorrectAnswer
if numberChallenge != nil {
if nonce == "" {
return nil, logical.ErrorResponse("nonce must be provided during login request when presented with number challenge"), nil, nil
}

b.verifyCache.SetDefault(nonce, *numberChallenge)
}

if err != nil {
return nil, logical.ErrorResponse(fmt.Sprintf("Okta auth failed checking loop: %v", err)), nil, nil
}
Expand Down
27 changes: 27 additions & 0 deletions builtin/credential/okta/cli.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package okta

import (
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/hashicorp/go-secure-stdlib/base62"
pwd "github.com/hashicorp/go-secure-stdlib/password"
"github.com/hashicorp/vault/api"
)
Expand Down Expand Up @@ -48,6 +51,30 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
data["provider"] = provider
}

nonce := base62.MustRandom(20)
data["nonce"] = nonce

// Create a done channel to signal termination of the login so that we can
// clean up the goroutine
doneCh := make(chan struct{})
defer close(doneCh)

go func() {
for {
select {
case <-doneCh:
return
case <-time.After(time.Second):
}

resp, _ := c.Logical().Read(fmt.Sprintf("auth/%s/verify/%s", mount, nonce))
calvn marked this conversation as resolved.
Show resolved Hide resolved
if resp != nil {
fmt.Fprintf(os.Stderr, "In Okta Verify, tap the number %q\n", resp.Data["correct_answer"].(json.Number))
return
}
}
}()

path := fmt.Sprintf("auth/%s/login/%s", mount, username)
secret, err := c.Logical().Write(path, data)
if err != nil {
Expand Down
49 changes: 47 additions & 2 deletions builtin/credential/okta/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ func pathLogin(b *backend) *framework.Path {
Type: framework.TypeString,
Description: "TOTP passcode.",
},
"nonce": {
Type: framework.TypeString,
Description: `Nonce provided if performing login that requires
number verification challenge. Logins through the vault login CLI command will
automatically generate a nonce.`,
},
"provider": {
Type: framework.TypeString,
Description: "Preferred factor provider.",
Expand Down Expand Up @@ -73,12 +79,15 @@ func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framew
username := d.Get("username").(string)
password := d.Get("password").(string)
totp := d.Get("totp").(string)
nonce := d.Get("nonce").(string)
preferredProvider := strings.ToUpper(d.Get("provider").(string))
if preferredProvider != "" && !strutil.StrListContains(b.getSupportedProviders(), preferredProvider) {
return logical.ErrorResponse(fmt.Sprintf("provider %s is not among the supported ones %v", preferredProvider, b.getSupportedProviders())), nil
}

policies, resp, groupNames, err := b.Login(ctx, req, username, password, totp, preferredProvider)
defer b.verifyCache.Delete(nonce)

policies, resp, groupNames, err := b.Login(ctx, req, username, password, totp, nonce, preferredProvider)
// Handle an internal error
if err != nil {
return nil, err
Expand Down Expand Up @@ -134,6 +143,7 @@ func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framew
func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := req.Auth.Metadata["username"]
password := req.Auth.InternalData["password"].(string)
nonce := d.Get("nonce").(string)

cfg, err := b.getConfig(ctx, req)
if err != nil {
Expand All @@ -142,7 +152,7 @@ func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *f

// No TOTP entry is possible on renew. If push MFA is enabled it will still be triggered, however.
// Sending "" as the totp will prompt the push action if it is configured.
loginPolicies, resp, groupNames, err := b.Login(ctx, req, username, password, "", "")
loginPolicies, resp, groupNames, err := b.Login(ctx, req, username, password, "", nonce, "")
if err != nil || (resp != nil && resp.IsError()) {
return resp, err
}
Expand Down Expand Up @@ -172,6 +182,41 @@ func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *f
return resp, nil
}

func pathVerify(b *backend) *framework.Path {
return &framework.Path{
Pattern: `verify/(?P<nonce>.+)`,
Fields: map[string]*framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Description: `Nonce provided during a login request to
retrieve the number verification challenge for the matching request.`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathVerify,
},
},
}
}

func (b *backend) pathVerify(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
nonce := d.Get("nonce").(string)

correctRaw, ok := b.verifyCache.Get(nonce)
if !ok {
return nil, nil
}

resp := &logical.Response{
Data: map[string]interface{}{
"correct_answer": correctRaw.(int),
},
}

return resp, nil
}

func (b *backend) getConfig(ctx context.Context, req *logical.Request) (*ConfigEntry, error) {
cfg, err := b.Config(ctx, req.Storage)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions changelog/15361.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
```release-note:improvement
auth/okta: Add support for performing [the number
calvn marked this conversation as resolved.
Show resolved Hide resolved
challenge](https://help.okta.com/en-us/Content/Topics/Mobile/ov-admin-config.htm?cshid=csh-okta-verify-number-challenge-v1#enable-number-challenge)
during an Okta Verify push challenge
```