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 <jkalafut@hashicorp.com>
This commit is contained in:
parent
da7fe65aee
commit
ca44b5a3e0
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<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 {
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -353,6 +353,8 @@ Login with the username and password.
|
|||
- `password` `(string: <required>)` - Password for the authenticating user.
|
||||
- `totp` `(string: <optional>)` - Okta Verify TOTP passcode.
|
||||
- `provider` `(string: <optional>)` - MFA TOTP factor provider. `GOOGLE` and `OKTA` are currently supported.
|
||||
- `nonce` `(string: <optional>)` - 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: <required>)` - 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
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in New Issue