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
20 changes: 19 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,13 @@ 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 {
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
17 changes: 17 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,20 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
data["provider"] = provider
}

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

go func() {
for {
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.Printf("In Okta Verify, tap the number '%s'\n", resp.Data["correctAnswer"].(json.Number))
calvn marked this conversation as resolved.
Show resolved Hide resolved
return
}
time.Sleep(time.Second)
}
}()

path := fmt.Sprintf("auth/%s/login/%s", mount, username)
secret, err := c.Logical().Write(path, data)
if err != nil {
Expand Down
51 changes: 49 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`,
calvn marked this conversation as resolved.
Show resolved Hide resolved
},
"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,43 @@ 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,
ForwardPerformanceSecondary: true,
ForwardPerformanceStandby: true,
calvn marked this conversation as resolved.
Show resolved Hide resolved
},
},
}
}

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{}{
"correctAnswer": correctRaw.(int),
calvn marked this conversation as resolved.
Show resolved Hide resolved
},
}

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:feature
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
```