From ca44b5a3e0a9ccc9ae77418413bca030c663b22d Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang <1883212+calvn@users.noreply.github.com> Date: Wed, 11 May 2022 17:09:29 -0700 Subject: [PATCH] auth/okta: Add support for Okta number challenge (#15361) * POC of Okta Auth Number Challenge verification * switch from callbacks to operations, forward validate to primary * cleanup and nonce description update * add changelog * error on empty nonce, no forwarding, return correct_answer instead * properly clean up verify goroutine * add docs on new endpoint and parameters * change polling frequency when WAITING to 1s Co-authored-by: Jim Kalafut --- builtin/credential/okta/backend.go | 26 ++++++++++++-- builtin/credential/okta/cli.go | 27 ++++++++++++++ builtin/credential/okta/path_login.go | 49 ++++++++++++++++++++++++-- changelog/15361.txt | 5 +++ website/content/api-docs/auth/okta.mdx | 47 +++++++++++++++++++++--- 5 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 changelog/15361.txt diff --git a/builtin/credential/okta/backend.go b/builtin/credential/okta/backend.go index 72ba13e6a..d7ac1d8cb 100644 --- a/builtin/credential/okta/backend.go +++ b/builtin/credential/okta/backend.go @@ -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 ( @@ -34,6 +35,7 @@ func Backend() *backend { PathsSpecial: &logical.Paths{ Unauthenticated: []string{ "login/*", + "verify/*", }, SealWrapStorage: []string{ "config", @@ -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 @@ -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 { @@ -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 } @@ -246,7 +268,7 @@ func (b *backend) Login(ctx context.Context, req *logical.Request, username, pas } select { - case <-time.After(500 * time.Millisecond): + case <-time.After(1 * time.Second): // Continue case <-ctx.Done(): return nil, logical.ErrorResponse("exiting pending mfa challenge"), nil, nil diff --git a/builtin/credential/okta/cli.go b/builtin/credential/okta/cli.go index cf82a09e1..f6e3d13b7 100644 --- a/builtin/credential/okta/cli.go +++ b/builtin/credential/okta/cli.go @@ -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" ) @@ -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)) + 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 { diff --git a/builtin/credential/okta/path_login.go b/builtin/credential/okta/path_login.go index 8402da4c9..c6b18f02d 100644 --- a/builtin/credential/okta/path_login.go +++ b/builtin/credential/okta/path_login.go @@ -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.", @@ -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 @@ -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 { @@ -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 } @@ -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.+)`, + 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 { diff --git a/changelog/15361.txt b/changelog/15361.txt new file mode 100644 index 000000000..1d4284d00 --- /dev/null +++ b/changelog/15361.txt @@ -0,0 +1,5 @@ +```release-note:improvement +auth/okta: Add support for performing [the number +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 +``` \ No newline at end of file diff --git a/website/content/api-docs/auth/okta.mdx b/website/content/api-docs/auth/okta.mdx index 789cfd596..a1609899e 100644 --- a/website/content/api-docs/auth/okta.mdx +++ b/website/content/api-docs/auth/okta.mdx @@ -353,6 +353,8 @@ Login with the username and password. - `password` `(string: )` - Password for the authenticating user. - `totp` `(string: )` - Okta Verify TOTP passcode. - `provider` `(string: )` - MFA TOTP factor provider. `GOOGLE` and `OKTA` are currently supported. +- `nonce` `(string: )` - Nonce provided during a login request to + retrieve the number verification challenge for the matching request. ### Sample Payload @@ -373,7 +375,7 @@ $ curl \ ### Sample Response -```javascript +```json { "lease_id": "", "renewable": false, @@ -388,8 +390,45 @@ $ curl \ "username": "fred", "policies": "default" }, - }, - "lease_duration": 7200, - "renewable": true + "lease_duration": 7200, + "renewable": true + } } ``` + +## Verify + +Verify a number challenge that may result from an Okta Verify Push challenge. + +| Method | Path | +| :----- | :--------------------------- | +| `GET` | `/auth/okta/verify/:nonce` | + +### Parameters + +- `nonce` `(string: )` - Nonce provided if performing login that +requires number verification challenge. Logins through the vault login CLI +command will automatically generate a nonce. + +### Sample Request + +```shell-session +$ curl \ + http://127.0.0.1:8200/v1/auth/okta/nonce/BCR66Ru6oJKPtC00PxJJ +``` + +### Sample Response + +```json +{ + "request_id": "de6a8029-79e5-1dd1-dbe9-6405166b3f94", + "lease_id": "", + "lease_duration": 0, + "renewable": false, + "data": { + "correct_answer": 94 + }, + "warnings": null +} +``` +