1
0

remove external mfa (pingid, duo, okta)

This commit is contained in:
Konstantin Demin 2024-07-01 20:37:46 +03:00
parent 2acf0a21fd
commit de439ac574
51 changed files with 58 additions and 4408 deletions

View File

@ -1,406 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package okta
import (
"context"
"fmt"
"net/textproto"
"time"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/vault/sdk/framework"
"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 (
operationPrefixOkta = "okta"
mfaPushMethod = "push"
mfaTOTPMethod = "token:software:totp"
)
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := Backend()
if err := b.Setup(ctx, conf); err != nil {
return nil, err
}
return b, nil
}
func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Help: backendHelp,
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"login/*",
"verify/*",
},
SealWrapStorage: []string{
"config",
},
},
Paths: []*framework.Path{
pathConfig(&b),
pathUsers(&b),
pathGroups(&b),
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, nonce, preferredProvider string) ([]string, *logical.Response, []string, error) {
cfg, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, nil, nil, err
}
if cfg == nil {
return nil, logical.ErrorResponse("Okta auth method not configured"), nil, nil
}
// Check for a CIDR match.
if len(cfg.TokenBoundCIDRs) > 0 {
if req.Connection == nil {
b.Logger().Warn("token bound CIDRs found but no connection information available for validation")
return nil, nil, nil, logical.ErrPermissionDenied
}
if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, cfg.TokenBoundCIDRs) {
return nil, nil, nil, logical.ErrPermissionDenied
}
}
shim, err := cfg.OktaClient(ctx)
if err != nil {
return nil, nil, nil, err
}
type mfaFactor struct {
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 {
Embedded embeddedResult `json:"_embedded"`
Status string `json:"status"`
FactorResult string `json:"factorResult"`
StateToken string `json:"stateToken"`
}
authReq, err := shim.NewRequest("POST", "authn", map[string]interface{}{
"username": username,
"password": password,
})
if err != nil {
return nil, nil, nil, err
}
var result authResult
rsp, err := shim.Do(authReq, &result)
if err != nil {
if oe, ok := err.(*okta.Error); ok {
return nil, logical.ErrorResponse("Okta auth failed: %v (code=%v)", err, oe.ErrorCode), nil, nil
}
return nil, logical.ErrorResponse(fmt.Sprintf("Okta auth failed: %v", err)), nil, nil
}
if rsp == nil {
return nil, logical.ErrorResponse("okta auth method unexpected failure"), nil, nil
}
oktaResponse := &logical.Response{
Data: map[string]interface{}{},
}
// More about Okta's Auth transaction state here:
// https://developer.okta.com/docs/api/resources/authn#transaction-state
// If lockout failures are not configured to be hidden, the status needs to
// be inspected for LOCKED_OUT status. Otherwise, it is handled above by an
// error returned during the authentication request.
switch result.Status {
case "LOCKED_OUT":
if b.Logger().IsDebug() {
b.Logger().Debug("user is locked out", "user", username)
}
return nil, logical.ErrorResponse("okta authentication failed"), nil, nil
case "PASSWORD_EXPIRED":
if b.Logger().IsDebug() {
b.Logger().Debug("password is expired", "user", username)
}
return nil, logical.ErrorResponse("okta authentication failed"), nil, nil
case "PASSWORD_WARN":
oktaResponse.AddWarning("Your Okta password is in warning state and needs to be changed soon.")
case "MFA_ENROLL", "MFA_ENROLL_ACTIVATE":
if !cfg.BypassOktaMFA {
if b.Logger().IsDebug() {
b.Logger().Debug("user must enroll or complete mfa enrollment", "user", username)
}
return nil, logical.ErrorResponse("okta authentication failed: you must complete MFA enrollment to continue"), nil, nil
}
case "MFA_REQUIRED":
// Per Okta documentation: Users are challenged for MFA (MFA_REQUIRED)
// before the Status of PASSWORD_EXPIRED is exposed (if they have an
// active factor enrollment). This bypass removes visibility
// into the authenticating user's password expiry, but still ensures the
// credentials are valid and the user is not locked out.
//
// API reference: https://developer.okta.com/docs/reference/api/authn/#verify-factor
if cfg.BypassOktaMFA {
result.Status = "SUCCESS"
break
}
var selectedFactor, totpFactor, pushFactor *mfaFactor
// Scan for available factors
for _, v := range result.Embedded.Factors {
v := v // create a new copy since we'll be taking the address later
if preferredProvider != "" && preferredProvider != v.Provider {
continue
}
if !strutil.StrListContains(b.getSupportedProviders(), v.Provider) {
continue
}
switch v.Type {
case mfaTOTPMethod:
totpFactor = &v
case mfaPushMethod:
pushFactor = &v
}
}
// Okta push and totp, and Google totp are currently supported.
// If a totp passcode is provided during login and is supported,
// that will be the preferred method.
switch {
case totpFactor != nil && totp != "":
selectedFactor = totpFactor
case pushFactor != nil && pushFactor.Provider == oktaProvider:
selectedFactor = pushFactor
case totpFactor != nil && totp == "":
return nil, logical.ErrorResponse("'totp' passcode parameter is required to perform MFA"), nil, nil
default:
return nil, logical.ErrorResponse("Okta Verify Push or TOTP or Google TOTP factor is required in order to perform MFA"), nil, nil
}
requestPath := fmt.Sprintf("authn/factors/%s/verify", selectedFactor.Id)
payload := map[string]interface{}{
"stateToken": result.StateToken,
}
if selectedFactor.Type == mfaTOTPMethod {
payload["passCode"] = totp
}
verifyReq, err := shim.NewRequest("POST", requestPath, payload)
if err != nil {
return nil, nil, nil, err
}
if len(req.Headers["X-Forwarded-For"]) > 0 {
verifyReq.Header.Set("X-Forwarded-For", req.Headers[textproto.CanonicalMIMEHeaderKey("X-Forwarded-For")][0])
}
rsp, err := shim.Do(verifyReq, &result)
if err != nil {
return nil, logical.ErrorResponse(fmt.Sprintf("Okta auth failed: %v", err)), nil, nil
}
if rsp == nil {
return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil, nil
}
for result.Status == "MFA_CHALLENGE" {
switch result.FactorResult {
case "WAITING":
verifyReq, err := shim.NewRequest("POST", requestPath, payload)
if err != nil {
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
}
if rsp == nil {
return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil, nil
}
timer := time.NewTimer(1 * time.Second)
select {
case <-timer.C:
// Continue
case <-ctx.Done():
timer.Stop()
return nil, logical.ErrorResponse("exiting pending mfa challenge"), nil, nil
}
case "REJECTED":
return nil, logical.ErrorResponse("multi-factor authentication denied"), nil, nil
case "TIMEOUT":
return nil, logical.ErrorResponse("failed to complete multi-factor authentication"), nil, nil
case "SUCCESS":
// Allowed
default:
if b.Logger().IsDebug() {
b.Logger().Debug("unhandled result status", "status", result.Status, "factorstatus", result.FactorResult)
}
return nil, logical.ErrorResponse("okta authentication failed"), nil, nil
}
}
case "SUCCESS":
// Do nothing here
default:
if b.Logger().IsDebug() {
b.Logger().Debug("unhandled result status", "status", result.Status)
}
return nil, logical.ErrorResponse("okta authentication failed"), nil, nil
}
// Verify result status again in case a switch case above modifies result
switch {
case result.Status == "SUCCESS",
result.Status == "PASSWORD_WARN",
result.Status == "MFA_REQUIRED" && cfg.BypassOktaMFA,
result.Status == "MFA_ENROLL" && cfg.BypassOktaMFA,
result.Status == "MFA_ENROLL_ACTIVATE" && cfg.BypassOktaMFA:
// Allowed
default:
if b.Logger().IsDebug() {
b.Logger().Debug("authentication returned a non-success status", "status", result.Status)
}
return nil, logical.ErrorResponse("okta authentication failed"), nil, nil
}
var allGroups []string
// Only query the Okta API for group membership if we have a token
client, oktactx := shim.Client()
if client != nil {
oktaGroups, err := b.getOktaGroups(oktactx, client, &result.Embedded.User)
if err != nil {
return nil, logical.ErrorResponse(fmt.Sprintf("okta failure retrieving groups: %v", err)), nil, nil
}
if len(oktaGroups) == 0 {
errString := fmt.Sprintf(
"no Okta groups found; only policies from locally-defined groups available")
oktaResponse.AddWarning(errString)
}
allGroups = append(allGroups, oktaGroups...)
}
// Import the custom added groups from okta backend
user, err := b.User(ctx, req.Storage, username)
if err != nil {
if b.Logger().IsDebug() {
b.Logger().Debug("error looking up user", "error", err)
}
}
if err == nil && user != nil && user.Groups != nil {
if b.Logger().IsDebug() {
b.Logger().Debug("adding local groups", "num_local_groups", len(user.Groups), "local_groups", user.Groups)
}
allGroups = append(allGroups, user.Groups...)
}
// Retrieve policies
var policies []string
for _, groupName := range allGroups {
entry, _, err := b.Group(ctx, req.Storage, groupName)
if err != nil {
if b.Logger().IsDebug() {
b.Logger().Debug("error looking up group policies", "error", err)
}
}
if err == nil && entry != nil && entry.Policies != nil {
policies = append(policies, entry.Policies...)
}
}
// Merge local Policies into Okta Policies
if user != nil && user.Policies != nil {
policies = append(policies, user.Policies...)
}
return policies, oktaResponse, allGroups, nil
}
func (b *backend) getOktaGroups(ctx context.Context, client *okta.Client, user *okta.User) ([]string, error) {
groups, resp, err := client.User.ListUserGroups(ctx, user.Id)
if err != nil {
return nil, err
}
oktaGroups := make([]string, 0, len(groups))
for _, group := range groups {
oktaGroups = append(oktaGroups, group.Profile.Name)
}
for resp.HasNextPage() {
var nextGroups []*okta.Group
resp, err = resp.Next(ctx, &nextGroups)
if err != nil {
return nil, err
}
for _, group := range nextGroups {
oktaGroups = append(oktaGroups, group.Profile.Name)
}
}
if b.Logger().IsDebug() {
b.Logger().Debug("Groups fetched from Okta", "num_groups", len(oktaGroups), "groups", fmt.Sprintf("%#v", oktaGroups))
}
return oktaGroups, nil
}
const backendHelp = `
The Okta credential provider allows authentication querying,
checking username and password, and associating policies. If an api token is
configured groups are pulled down from Okta.
Configuration of the connection is done through the "config" and "policies"
endpoints by a user with root access. Authentication is then done
by supplying the two fields for "login".
`

View File

@ -1,300 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package okta
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/helper/testhelpers"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/okta/okta-sdk-golang/v2/okta"
"github.com/okta/okta-sdk-golang/v2/okta/query"
"github.com/stretchr/testify/require"
)
// To run this test, set the following env variables:
// VAULT_ACC=1
// OKTA_ORG=dev-219337
// OKTA_API_TOKEN=<generate via web UI, see Confluence for login details>
// OKTA_USERNAME=test3@example.com
// OKTA_PASSWORD=<find in 1password>
//
// You will need to install the Okta client app on your mobile device and
// setup MFA in order to use the Okta web UI. This test does not exercise
// MFA however (which is an enterprise feature), and therefore the test
// user in OKTA_USERNAME should not be configured with it. Currently
// test3@example.com is not a member of testgroup, which is the group with
// the profile that requires MFA.
func TestBackend_Config(t *testing.T) {
if os.Getenv("VAULT_ACC") == "" {
t.SkipNow()
}
// Ensure each cred is populated.
credNames := []string{
"OKTA_USERNAME",
"OKTA_PASSWORD",
"OKTA_API_TOKEN",
}
testhelpers.SkipUnlessEnvVarsSet(t, credNames)
defaultLeaseTTLVal := time.Hour * 12
maxLeaseTTLVal := time.Hour * 24
b, err := Factory(context.Background(), &logical.BackendConfig{
Logger: logging.NewVaultLogger(log.Trace),
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: defaultLeaseTTLVal,
MaxLeaseTTLVal: maxLeaseTTLVal,
},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}
username := os.Getenv("OKTA_USERNAME")
password := os.Getenv("OKTA_PASSWORD")
token := os.Getenv("OKTA_API_TOKEN")
groupIDs := createOktaGroups(t, username, token, os.Getenv("OKTA_ORG"))
defer deleteOktaGroups(t, token, os.Getenv("OKTA_ORG"), groupIDs)
configData := map[string]interface{}{
"org_name": os.Getenv("OKTA_ORG"),
"base_url": "oktapreview.com",
}
updatedDuration := time.Hour * 1
configDataToken := map[string]interface{}{
"api_token": token,
"token_ttl": "1h",
}
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
PreCheck: func() { testAccPreCheck(t) },
CredentialBackend: b,
Steps: []logicaltest.TestStep{
testConfigCreate(t, configData),
// 2. Login with bad password, expect failure (E0000004=okta auth failure).
testLoginWrite(t, username, "wrong", "E0000004", 0, nil),
// 3. Make our user belong to two groups and have one user-specific policy.
testAccUserGroups(t, username, "local_grouP,lOcal_group2", []string{"user_policy"}),
// 4. Create the group local_group, assign it a single policy.
testAccGroups(t, "local_groUp", "loCal_group_policy"),
// 5. Login with good password, expect user to have their user-specific
// policy and the policy of the one valid group they belong to.
testLoginWrite(t, username, password, "", defaultLeaseTTLVal, []string{"local_group_policy", "user_policy"}),
// 6. Create the group everyone, assign it two policies. This is a
// magic group name in okta that always exists and which every
// user automatically belongs to.
testAccGroups(t, "everyoNe", "everyone_grouP_policy,eveRy_group_policy2"),
// 7. Login as before, expect same result
testLoginWrite(t, username, password, "", defaultLeaseTTLVal, []string{"local_group_policy", "user_policy"}),
// 8. Add API token so we can lookup groups
testConfigUpdate(t, configDataToken),
testConfigRead(t, token, configData),
// 10. Login should now lookup okta groups; since all okta users are
// in the "everyone" group, that should be returned; since we
// defined policies attached to the everyone group, we should now
// see those policies attached to returned vault token.
testLoginWrite(t, username, password, "", updatedDuration, []string{"everyone_group_policy", "every_group_policy2", "local_group_policy", "user_policy"}),
testAccGroups(t, "locAl_group2", "testgroup_group_policy"),
testLoginWrite(t, username, password, "", updatedDuration, []string{"everyone_group_policy", "every_group_policy2", "local_group_policy", "testgroup_group_policy", "user_policy"}),
},
})
}
func createOktaGroups(t *testing.T, username string, token string, org string) []string {
orgURL := "https://" + org + "." + previewBaseURL
ctx, client, err := okta.NewClient(context.Background(), okta.WithOrgUrl(orgURL), okta.WithToken(token))
require.Nil(t, err)
users, _, err := client.User.ListUsers(ctx, &query.Params{
Q: username,
})
require.Nil(t, err)
require.Len(t, users, 1)
userID := users[0].Id
var groupIDs []string
// Verify that login's call to list the groups of the user logging in will page
// through multiple result sets; note here
// https://developer.okta.com/docs/reference/api/groups/#list-groups-with-defaults
// that "If you don't specify a value for limit and don't specify a query,
// only 200 results are returned for most orgs."
for i := 0; i < 201; i++ {
name := fmt.Sprintf("TestGroup%d", i)
groups, _, err := client.Group.ListGroups(ctx, &query.Params{
Q: name,
})
require.Nil(t, err)
var groupID string
if len(groups) == 0 {
group, _, err := client.Group.CreateGroup(ctx, okta.Group{
Profile: &okta.GroupProfile{
Name: fmt.Sprintf("TestGroup%d", i),
},
})
require.Nil(t, err)
groupID = group.Id
} else {
groupID = groups[0].Id
}
groupIDs = append(groupIDs, groupID)
_, err = client.Group.AddUserToGroup(ctx, groupID, userID)
require.Nil(t, err)
}
return groupIDs
}
func deleteOktaGroups(t *testing.T, token string, org string, groupIDs []string) {
orgURL := "https://" + org + "." + previewBaseURL
ctx, client, err := okta.NewClient(context.Background(), okta.WithOrgUrl(orgURL), okta.WithToken(token))
require.Nil(t, err)
for _, groupID := range groupIDs {
_, err := client.Group.DeleteGroup(ctx, groupID)
require.Nil(t, err)
}
}
func testLoginWrite(t *testing.T, username, password, reason string, expectedTTL time.Duration, policies []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + username,
ErrorOk: true,
Data: map[string]interface{}{
"password": password,
},
Check: func(resp *logical.Response) error {
if resp.IsError() {
if reason == "" || !strings.Contains(resp.Error().Error(), reason) {
return resp.Error()
}
} else if reason != "" {
return fmt.Errorf("expected error containing %q, got no error", reason)
}
if resp.Auth != nil {
if !policyutil.EquivalentPolicies(resp.Auth.Policies, policies) {
return fmt.Errorf("policy mismatch expected %v but got %v", policies, resp.Auth.Policies)
}
actualTTL := resp.Auth.LeaseOptions.TTL
if actualTTL != expectedTTL {
return fmt.Errorf("TTL mismatch expected %v but got %v", expectedTTL, actualTTL)
}
}
return nil
},
}
}
func testConfigCreate(t *testing.T, d map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.CreateOperation,
Path: "config",
Data: d,
}
}
func testConfigUpdate(t *testing.T, d map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
Data: d,
}
}
func testConfigRead(t *testing.T, token string, d map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "config",
Check: func(resp *logical.Response) error {
if resp.IsError() {
return resp.Error()
}
if resp.Data["org_name"] != d["org_name"] {
return fmt.Errorf("org mismatch expected %s but got %s", d["organization"], resp.Data["Org"])
}
if resp.Data["base_url"] != d["base_url"] {
return fmt.Errorf("BaseURL mismatch expected %s but got %s", d["base_url"], resp.Data["BaseURL"])
}
for _, value := range resp.Data {
if value == token {
return fmt.Errorf("token should not be returned on a read request")
}
}
return nil
},
}
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("OKTA_USERNAME"); v == "" {
t.Fatal("OKTA_USERNAME must be set for acceptance tests")
}
if v := os.Getenv("OKTA_PASSWORD"); v == "" {
t.Fatal("OKTA_PASSWORD must be set for acceptance tests")
}
if v := os.Getenv("OKTA_ORG"); v == "" {
t.Fatal("OKTA_ORG must be set for acceptance tests")
}
if v := os.Getenv("OKTA_API_TOKEN"); v == "" {
t.Fatal("OKTA_API_TOKEN must be set for acceptance tests")
}
}
func testAccUserGroups(t *testing.T, user string, groups interface{}, policies interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "users/" + user,
Data: map[string]interface{}{
"groups": groups,
"policies": policies,
},
}
}
func testAccGroups(t *testing.T, group string, policies interface{}) logicaltest.TestStep {
t.Logf("[testAccGroups] - Registering group %s, policy %s", group, policies)
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "groups/" + group,
Data: map[string]interface{}{
"policies": policies,
},
}
}
func testAccLogin(t *testing.T, user, password string, keys []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": password,
},
Unauthenticated: true,
Check: logicaltest.TestCheckAuth(keys),
}
}

View File

@ -1,122 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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"
)
// CLIHandler struct
type CLIHandler struct{}
// Auth cli method
func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) {
mount, ok := m["mount"]
if !ok {
mount = "okta"
}
username, ok := m["username"]
if !ok {
return nil, fmt.Errorf("'username' var must be set")
}
password, ok := m["password"]
if !ok {
fmt.Fprintf(os.Stderr, "Password (will be hidden): ")
var err error
password, err = pwd.Read(os.Stdin)
fmt.Fprintf(os.Stderr, "\n")
if err != nil {
return nil, err
}
}
data := map[string]interface{}{
"password": password,
}
// Okta or Google totp code
if totp, ok := m["totp"]; ok {
data["totp"] = totp
}
// provider is an optional parameter
if provider, ok := m["provider"]; ok {
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 {
timer := time.NewTimer(time.Second)
select {
case <-doneCh:
timer.Stop()
return
case <-timer.C:
}
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 {
return nil, err
}
if secret == nil {
return nil, fmt.Errorf("empty response from credential provider")
}
return secret, nil
}
// Help method for okta cli
func (h *CLIHandler) Help() string {
help := `
Usage: vault login -method=okta [CONFIG K=V...]
The Okta auth method allows users to authenticate using Okta.
Authenticate as "sally":
$ vault login -method=okta username=sally
Password (will be hidden):
Authenticate as "bob":
$ vault login -method=okta username=bob password=password
Configuration:
password=<string>
Okta password to use for authentication. If not provided, the CLI will
prompt for this on stdin.
username=<string>
Okta username to use for authentication.
`
return strings.TrimSpace(help)
}

View File

@ -1,34 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package main
import (
"os"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/credential/okta"
"github.com/hashicorp/vault/sdk/plugin"
)
func main() {
apiClientMeta := &api.PluginAPIClientMeta{}
flags := apiClientMeta.FlagSet()
flags.Parse(os.Args[1:])
tlsConfig := apiClientMeta.GetTLSConfig()
tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig)
if err := plugin.ServeMultiplex(&plugin.ServeOpts{
BackendFactoryFunc: okta.Factory,
// set the TLSProviderFunc so that the plugin maintains backwards
// compatibility with Vault versions that dont support plugin AutoMTLS
TLSProviderFunc: tlsProviderFunc,
}); err != nil {
logger := hclog.New(&hclog.LoggerOptions{})
logger.Error("plugin shutting down", "error", err)
os.Exit(1)
}
}

View File

@ -1,381 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package okta
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"time"
oktaold "github.com/chrismalek/oktasdk-go/okta"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/tokenutil"
"github.com/hashicorp/vault/sdk/logical"
oktanew "github.com/okta/okta-sdk-golang/v2/okta"
)
const (
defaultBaseURL = "okta.com"
previewBaseURL = "oktapreview.com"
)
func pathConfig(b *backend) *framework.Path {
p := &framework.Path{
Pattern: `config`,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixOkta,
Action: "Configure",
},
Fields: map[string]*framework.FieldSchema{
"organization": {
Type: framework.TypeString,
Description: "Use org_name instead.",
Deprecated: true,
},
"org_name": {
Type: framework.TypeString,
Description: "Name of the organization to be used in the Okta API.",
DisplayAttrs: &framework.DisplayAttributes{
Name: "Organization Name",
},
},
"token": {
Type: framework.TypeString,
Description: "Use api_token instead.",
Deprecated: true,
},
"api_token": {
Type: framework.TypeString,
Description: "Okta API key.",
DisplayAttrs: &framework.DisplayAttributes{
Name: "API Token",
},
},
"base_url": {
Type: framework.TypeString,
Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Base URL",
},
},
"production": {
Type: framework.TypeBool,
Description: `Use base_url instead.`,
Deprecated: true,
},
"ttl": {
Type: framework.TypeDurationSecond,
Description: tokenutil.DeprecationText("token_ttl"),
Deprecated: true,
},
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: tokenutil.DeprecationText("token_max_ttl"),
Deprecated: true,
},
"bypass_okta_mfa": {
Type: framework.TypeBool,
Description: `When set true, requests by Okta for a MFA check will be bypassed. This also disallows certain status checks on the account, such as whether the password is expired.`,
DisplayAttrs: &framework.DisplayAttributes{
Name: "Bypass Okta MFA",
},
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathConfigRead,
DisplayAttrs: &framework.DisplayAttributes{
OperationSuffix: "configuration",
},
},
logical.CreateOperation: &framework.PathOperation{
Callback: b.pathConfigWrite,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "configure",
},
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.pathConfigWrite,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "configure",
},
},
},
ExistenceCheck: b.pathConfigExistenceCheck,
HelpSynopsis: pathConfigHelp,
}
tokenutil.AddTokenFields(p.Fields)
p.Fields["token_policies"].Description += ". This will apply to all tokens generated by this auth method, in addition to any configured for specific users/groups."
return p
}
// Config returns the configuration for this backend.
func (b *backend) Config(ctx context.Context, s logical.Storage) (*ConfigEntry, error) {
entry, err := s.Get(ctx, "config")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result ConfigEntry
if entry != nil {
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
}
if result.TokenTTL == 0 && result.TTL > 0 {
result.TokenTTL = result.TTL
}
if result.TokenMaxTTL == 0 && result.MaxTTL > 0 {
result.TokenMaxTTL = result.MaxTTL
}
return &result, nil
}
func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, err
}
if cfg == nil {
return nil, nil
}
data := map[string]interface{}{
"organization": cfg.Org,
"org_name": cfg.Org,
"bypass_okta_mfa": cfg.BypassOktaMFA,
}
cfg.PopulateTokenData(data)
if cfg.BaseURL != "" {
data["base_url"] = cfg.BaseURL
}
if cfg.Production != nil {
data["production"] = *cfg.Production
}
if cfg.TTL > 0 {
data["ttl"] = int64(cfg.TTL.Seconds())
}
if cfg.MaxTTL > 0 {
data["max_ttl"] = int64(cfg.MaxTTL.Seconds())
}
resp := &logical.Response{
Data: data,
}
if cfg.BypassOktaMFA {
resp.AddWarning("Okta MFA bypass is configured. In addition to ignoring Okta MFA requests, certain other account statuses will not be seen, such as PASSWORD_EXPIRED. Authentication will succeed in these cases.")
}
return resp, nil
}
func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, err
}
// Due to the existence check, entry will only be nil if it's a create
// operation, so just create a new one
if cfg == nil {
cfg = &ConfigEntry{}
}
org, ok := d.GetOk("org_name")
if ok {
cfg.Org = org.(string)
}
if cfg.Org == "" {
org, ok = d.GetOk("organization")
if ok {
cfg.Org = org.(string)
}
}
if cfg.Org == "" && req.Operation == logical.CreateOperation {
return logical.ErrorResponse("org_name is missing"), nil
}
token, ok := d.GetOk("api_token")
if ok {
cfg.Token = token.(string)
} else if token, ok = d.GetOk("token"); ok {
cfg.Token = token.(string)
}
baseURLRaw, ok := d.GetOk("base_url")
if ok {
baseURL := baseURLRaw.(string)
_, err = url.Parse(fmt.Sprintf("https://%s,%s", cfg.Org, baseURL))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error parsing given base_url: %s", err)), nil
}
cfg.BaseURL = baseURL
}
// We only care about the production flag when base_url is not set. It is
// for compatibility reasons.
if cfg.BaseURL == "" {
productionRaw, ok := d.GetOk("production")
if ok {
production := productionRaw.(bool)
cfg.Production = &production
}
} else {
// clear out old production flag if base_url is set
cfg.Production = nil
}
bypass, ok := d.GetOk("bypass_okta_mfa")
if ok {
cfg.BypassOktaMFA = bypass.(bool)
}
if err := cfg.ParseTokenFields(req, d); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
// Handle upgrade cases
{
if err := tokenutil.UpgradeValue(d, "ttl", "token_ttl", &cfg.TTL, &cfg.TokenTTL); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
if err := tokenutil.UpgradeValue(d, "max_ttl", "token_max_ttl", &cfg.MaxTTL, &cfg.TokenMaxTTL); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
}
jsonCfg, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, jsonCfg); err != nil {
return nil, err
}
var resp *logical.Response
if cfg.BypassOktaMFA {
resp = new(logical.Response)
resp.AddWarning("Okta MFA bypass is configured. In addition to ignoring Okta MFA requests, certain other account statuses will not be seen, such as PASSWORD_EXPIRED. Authentication will succeed in these cases.")
}
return resp, nil
}
func (b *backend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) {
cfg, err := b.Config(ctx, req.Storage)
if err != nil {
return false, err
}
return cfg != nil, nil
}
type oktaShim interface {
Client() (*oktanew.Client, context.Context)
NewRequest(method string, url string, body interface{}) (*http.Request, error)
Do(req *http.Request, v interface{}) (interface{}, error)
}
type oktaShimNew struct {
client *oktanew.Client
ctx context.Context
}
func (new *oktaShimNew) Client() (*oktanew.Client, context.Context) {
return new.client, new.ctx
}
func (new *oktaShimNew) NewRequest(method string, url string, body interface{}) (*http.Request, error) {
if !strings.HasPrefix(url, "/") {
url = "/api/v1/" + url
}
return new.client.GetRequestExecutor().NewRequest(method, url, body)
}
func (new *oktaShimNew) Do(req *http.Request, v interface{}) (interface{}, error) {
return new.client.GetRequestExecutor().Do(new.ctx, req, v)
}
type oktaShimOld struct {
client *oktaold.Client
}
func (new *oktaShimOld) Client() (*oktanew.Client, context.Context) {
return nil, nil
}
func (new *oktaShimOld) NewRequest(method string, url string, body interface{}) (*http.Request, error) {
return new.client.NewRequest(method, url, body)
}
func (new *oktaShimOld) Do(req *http.Request, v interface{}) (interface{}, error) {
return new.client.Do(req, v)
}
// OktaClient creates a basic okta client connection
func (c *ConfigEntry) OktaClient(ctx context.Context) (oktaShim, error) {
baseURL := defaultBaseURL
if c.Production != nil {
if !*c.Production {
baseURL = previewBaseURL
}
}
if c.BaseURL != "" {
baseURL = c.BaseURL
}
if c.Token != "" {
ctx, client, err := oktanew.NewClient(ctx,
oktanew.WithOrgUrl("https://"+c.Org+"."+baseURL),
oktanew.WithToken(c.Token))
if err != nil {
return nil, err
}
return &oktaShimNew{client, ctx}, nil
}
client, err := oktaold.NewClientWithDomain(cleanhttp.DefaultClient(), c.Org, baseURL, "")
if err != nil {
return nil, err
}
return &oktaShimOld{client}, nil
}
// ConfigEntry for Okta
type ConfigEntry struct {
tokenutil.TokenParams
Org string `json:"organization"`
Token string `json:"token"`
BaseURL string `json:"base_url"`
Production *bool `json:"is_production,omitempty"`
TTL time.Duration `json:"ttl"`
MaxTTL time.Duration `json:"max_ttl"`
BypassOktaMFA bool `json:"bypass_okta_mfa"`
}
const pathConfigHelp = `
This endpoint allows you to configure the Okta and its
configuration options.
The Okta organization are the characters at the front of the URL for Okta.
Example https://ORG.okta.com
`

View File

@ -1,219 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package okta
import (
"context"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/logical"
)
func pathGroupsList(b *backend) *framework.Path {
return &framework.Path{
Pattern: "groups/?$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixOkta,
OperationSuffix: "groups",
Navigation: true,
ItemType: "Group",
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathGroupList,
},
HelpSynopsis: pathGroupHelpSyn,
HelpDescription: pathGroupHelpDesc,
}
}
func pathGroups(b *backend) *framework.Path {
return &framework.Path{
Pattern: `groups/(?P<name>.+)`,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixOkta,
OperationSuffix: "group",
Action: "Create",
ItemType: "Group",
},
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the Okta group.",
},
"policies": {
Type: framework.TypeCommaStringSlice,
Description: "Comma-separated list of policies associated to the group.",
DisplayAttrs: &framework.DisplayAttributes{
Description: "A list of policies associated to the group.",
},
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.DeleteOperation: b.pathGroupDelete,
logical.ReadOperation: b.pathGroupRead,
logical.UpdateOperation: b.pathGroupWrite,
},
HelpSynopsis: pathGroupHelpSyn,
HelpDescription: pathGroupHelpDesc,
}
}
// We look up groups in a case-insensitive manner since Okta is case-preserving
// but case-insensitive for comparisons
func (b *backend) Group(ctx context.Context, s logical.Storage, n string) (*GroupEntry, string, error) {
canonicalName := n
entry, err := s.Get(ctx, "group/"+n)
if err != nil {
return nil, "", err
}
if entry == nil {
entries, err := groupList(ctx, s)
if err != nil {
return nil, "", err
}
for _, groupName := range entries {
if strings.EqualFold(groupName, n) {
entry, err = s.Get(ctx, "group/"+groupName)
if err != nil {
return nil, "", err
}
canonicalName = groupName
break
}
}
}
if entry == nil {
return nil, "", nil
}
var result GroupEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, "", err
}
return &result, canonicalName, nil
}
func (b *backend) pathGroupDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
if len(name) == 0 {
return logical.ErrorResponse("'name' must be supplied"), nil
}
entry, canonicalName, err := b.Group(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if entry != nil {
err := req.Storage.Delete(ctx, "group/"+canonicalName)
if err != nil {
return nil, err
}
}
return nil, nil
}
func (b *backend) pathGroupRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
if len(name) == 0 {
return logical.ErrorResponse("'name' must be supplied"), nil
}
group, _, err := b.Group(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if group == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"policies": group.Policies,
},
}, nil
}
func (b *backend) pathGroupWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
if len(name) == 0 {
return logical.ErrorResponse("'name' must be supplied"), nil
}
// Check for an existing group, possibly lowercased so that we keep using
// existing user set values
_, canonicalName, err := b.Group(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if canonicalName != "" {
name = canonicalName
} else {
name = strings.ToLower(name)
}
entry, err := logical.StorageEntryJSON("group/"+name, &GroupEntry{
Policies: policyutil.ParsePolicies(d.Get("policies")),
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathGroupList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
groups, err := groupList(ctx, req.Storage)
if err != nil {
return nil, err
}
return logical.ListResponse(groups), nil
}
func groupList(ctx context.Context, s logical.Storage) ([]string, error) {
keys, err := logical.CollectKeysWithPrefix(ctx, s, "group/")
if err != nil {
return nil, err
}
for i := range keys {
keys[i] = strings.TrimPrefix(keys[i], "group/")
}
return keys, nil
}
type GroupEntry struct {
Policies []string
}
const pathGroupHelpSyn = `
Manage users allowed to authenticate.
`
const pathGroupHelpDesc = `
This endpoint allows you to create, read, update, and delete configuration
for Okta groups that are allowed to authenticate, and associate policies to
them.
Deleting a group will not revoke auth for prior authenticated users in that
group. To do this, do a revoke on "login/<username>" for
the usernames you want revoked.
`

View File

@ -1,111 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package okta
import (
"context"
"strings"
"testing"
"time"
"github.com/go-test/deep"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/logical"
)
func TestGroupsList(t *testing.T) {
b, storage := getBackend(t)
groups := []string{
"%20\\",
"foo",
"zfoo",
"🙂",
"foo/nested",
"foo/even/more/nested",
}
for _, group := range groups {
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "groups/" + group,
Storage: storage,
Data: map[string]interface{}{
"policies": []string{group + "_a", group + "_b"},
},
}
resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
}
for _, group := range groups {
for _, upper := range []bool{false, true} {
groupPath := group
if upper {
groupPath = strings.ToUpper(group)
}
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "groups/" + groupPath,
Storage: storage,
}
resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if resp == nil {
t.Fatal("unexpected nil response")
}
expected := []string{group + "_a", group + "_b"}
if diff := deep.Equal(resp.Data["policies"].([]string), expected); diff != nil {
t.Fatal(diff)
}
}
}
req := &logical.Request{
Operation: logical.ListOperation,
Path: "groups",
Storage: storage,
}
resp, err := b.HandleRequest(context.Background(), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if diff := deep.Equal(resp.Data["keys"].([]string), groups); diff != nil {
t.Fatal(diff)
}
}
func getBackend(t *testing.T) (logical.Backend, logical.Storage) {
defaultLeaseTTLVal := time.Hour * 12
maxLeaseTTLVal := time.Hour * 24
config := &logical.BackendConfig{
Logger: logging.NewVaultLogger(log.Trace),
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: defaultLeaseTTLVal,
MaxLeaseTTLVal: maxLeaseTTLVal,
},
StorageView: &logical.InmemStorage{},
}
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatalf("unable to create backend: %v", err)
}
return b, config.StorageView
}

View File

@ -1,255 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package okta
import (
"context"
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
)
const (
googleProvider = "GOOGLE"
oktaProvider = "OKTA"
)
func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: `login/(?P<username>.+)`,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixOkta,
OperationVerb: "login",
},
Fields: map[string]*framework.FieldSchema{
"username": {
Type: framework.TypeString,
Description: "Username to be used for login.",
},
"password": {
Type: framework.TypeString,
Description: "Password for this user.",
},
"totp": {
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.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathLogin,
logical.AliasLookaheadOperation: b.pathLoginAliasLookahead,
},
HelpSynopsis: pathLoginSyn,
HelpDescription: pathLoginDesc,
}
}
func (b *backend) getSupportedProviders() []string {
return []string{googleProvider, oktaProvider}
}
func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := d.Get("username").(string)
if username == "" {
return nil, fmt.Errorf("missing username")
}
return &logical.Response{
Auth: &logical.Auth{
Alias: &logical.Alias{
Name: username,
},
},
}, nil
}
func (b *backend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
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
}
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
}
if resp != nil {
// Handle a logical error
if resp.IsError() {
return resp, nil
}
} else {
resp = &logical.Response{}
}
cfg, err := b.getConfig(ctx, req)
if err != nil {
return nil, err
}
auth := &logical.Auth{
Metadata: map[string]string{
"username": username,
"policies": strings.Join(policies, ","),
},
InternalData: map[string]interface{}{
"password": password,
},
DisplayName: username,
Alias: &logical.Alias{
Name: username,
},
}
cfg.PopulateTokenAuth(auth)
// Add in configured policies from mappings
if len(policies) > 0 {
auth.Policies = append(auth.Policies, policies...)
}
resp.Auth = auth
for _, groupName := range groupNames {
if groupName == "" {
continue
}
resp.Auth.GroupAliases = append(resp.Auth.GroupAliases, &logical.Alias{
Name: groupName,
})
}
return resp, nil
}
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)
var nonce string
if d != nil {
nonce = d.Get("nonce").(string)
}
cfg, err := b.getConfig(ctx, req)
if err != nil {
return nil, err
}
// 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, "", nonce, "")
if err != nil || (resp != nil && resp.IsError()) {
return resp, err
}
finalPolicies := cfg.TokenPolicies
if len(loginPolicies) > 0 {
finalPolicies = append(finalPolicies, loginPolicies...)
}
if !policyutil.EquivalentPolicies(finalPolicies, req.Auth.TokenPolicies) {
return nil, fmt.Errorf("policies have changed, not renewing")
}
resp.Auth = req.Auth
resp.Auth.Period = cfg.TokenPeriod
resp.Auth.TTL = cfg.TokenTTL
resp.Auth.MaxTTL = cfg.TokenMaxTTL
// Remove old aliases
resp.Auth.GroupAliases = nil
for _, groupName := range groupNames {
resp.Auth.GroupAliases = append(resp.Auth.GroupAliases, &logical.Alias{
Name: groupName,
})
}
return resp, nil
}
func pathVerify(b *backend) *framework.Path {
return &framework.Path{
Pattern: `verify/(?P<nonce>.+)`,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixOkta,
OperationVerb: "verify",
},
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 {
return nil, err
}
if cfg == nil {
return nil, errors.New("Okta backend not configured")
}
return cfg, nil
}
const pathLoginSyn = `
Log in with a username and password.
`
const pathLoginDesc = `
This endpoint authenticates using a username and password.
`

View File

@ -1,173 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package okta
import (
"context"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
func pathUsersList(b *backend) *framework.Path {
return &framework.Path{
Pattern: "users/?$",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixOkta,
OperationSuffix: "users",
Navigation: true,
ItemType: "User",
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ListOperation: b.pathUserList,
},
HelpSynopsis: pathUserHelpSyn,
HelpDescription: pathUserHelpDesc,
}
}
func pathUsers(b *backend) *framework.Path {
return &framework.Path{
Pattern: `users/(?P<name>.+)`,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixOkta,
OperationSuffix: "user",
Action: "Create",
ItemType: "User",
},
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "Name of the user.",
},
"groups": {
Type: framework.TypeCommaStringSlice,
Description: "List of groups associated with the user.",
},
"policies": {
Type: framework.TypeCommaStringSlice,
Description: "List of policies associated with the user.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.DeleteOperation: b.pathUserDelete,
logical.ReadOperation: b.pathUserRead,
logical.UpdateOperation: b.pathUserWrite,
},
HelpSynopsis: pathUserHelpSyn,
HelpDescription: pathUserHelpDesc,
}
}
func (b *backend) User(ctx context.Context, s logical.Storage, n string) (*UserEntry, error) {
entry, err := s.Get(ctx, "user/"+n)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result UserEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathUserDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
if len(name) == 0 {
return logical.ErrorResponse("Error empty name"), nil
}
err := req.Storage.Delete(ctx, "user/"+name)
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathUserRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
if len(name) == 0 {
return logical.ErrorResponse("Error empty name"), nil
}
user, err := b.User(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if user == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"groups": user.Groups,
"policies": user.Policies,
},
}, nil
}
func (b *backend) pathUserWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
if len(name) == 0 {
return logical.ErrorResponse("Error empty name"), nil
}
groups := d.Get("groups").([]string)
policies := d.Get("policies").([]string)
// Store it
entry, err := logical.StorageEntryJSON("user/"+name, &UserEntry{
Groups: groups,
Policies: policies,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathUserList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
users, err := req.Storage.List(ctx, "user/")
if err != nil {
return nil, err
}
return logical.ListResponse(users), nil
}
type UserEntry struct {
Groups []string
Policies []string
}
const pathUserHelpSyn = `
Manage additional groups for users allowed to authenticate.
`
const pathUserHelpDesc = `
This endpoint allows you to create, read, update, and delete configuration
for Okta users that are allowed to authenticate, in particular associating
additional groups to them.
Deleting a user will not revoke their auth. To do this, do a revoke on "login/<username>" for
the usernames you want revoked.
`

View File

@ -107,7 +107,6 @@ func (b *BaseCommand) PredictVaultAvailableAuths() complete.Predictor {
"cert",
"github",
"ldap",
"okta",
"plugin",
"radius",
"userpass",

View File

@ -33,7 +33,6 @@ import (
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
credOkta "github.com/hashicorp/vault/builtin/credential/okta"
credToken "github.com/hashicorp/vault/builtin/credential/token"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
@ -187,7 +186,6 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co
"kerberos": &credKerb.CLIHandler{},
"ldap": &credLdap.CLIHandler{},
"oidc": &credOIDC.CLIHandler{},
"okta": &credOkta.CLIHandler{},
"pcf": &credCF.CLIHandler{}, // Deprecated.
"radius": &credUserpass.CLIHandler{
DefaultMount: "radius",

7
go.mod
View File

@ -31,9 +31,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
github.com/axiomhq/hyperloglog v0.0.0-20220105174342-98591331716a
github.com/cenkalti/backoff/v3 v3.2.2
github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.16.0
github.com/fatih/structs v1.1.0
@ -45,7 +43,6 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/go-test/deep v1.1.0
github.com/gocql/gocql v1.0.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang/protobuf v1.5.3
github.com/google/go-cmp v0.6.0
github.com/google/go-github v17.0.0+incompatible
@ -136,7 +133,6 @@ require (
github.com/mitchellh/reflectwalk v1.0.2
github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc
github.com/oklog/run v1.1.0
github.com/okta/okta-sdk-golang/v2 v2.12.1
github.com/ory/dockertest v3.3.5+incompatible
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pires/go-proxyproto v0.6.1
@ -208,7 +204,6 @@ require (
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/centrify/cloud-golang-sdk v0.0.0-20210923165758-a8c48d049166 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect
@ -253,6 +248,7 @@ require (
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
@ -298,7 +294,6 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect

8
go.sum
View File

@ -1054,8 +1054,6 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0 h1:CWU8piLyqoi9qXEUwzOh5KFKGgmSU5ZhktJyYcq6ryQ=
github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0/go.mod h1:5d8DqS60xkj9k3aXfL3+mXBH0DPYO0FQjcKosxl+b/Q=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -1336,8 +1334,6 @@ github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 h1:2MIhn2R6oXQbgW5yHfS+d6YqyMfXiu2L55rFZC4UD/M=
github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74/go.mod h1:UqXY1lYT/ERa4OEAywUqdok1T4RCRdArkhic1Opuavo=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@ -2190,8 +2186,6 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -2426,8 +2420,6 @@ 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.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/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=

View File

@ -20,7 +20,6 @@ import (
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
credOkta "github.com/hashicorp/vault/builtin/credential/okta"
credRadius "github.com/hashicorp/vault/builtin/credential/radius"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
logicalNomad "github.com/hashicorp/vault/builtin/logical/nomad"
@ -91,7 +90,6 @@ func newRegistry() *registry {
"kubernetes": {Factory: credKube.Factory},
"ldap": {Factory: credLdap.Factory},
"oidc": {Factory: credJWT.Factory},
"okta": {Factory: credOkta.Factory},
"pcf": {
Factory: credCF.Factory,
DeprecationStatus: consts.Deprecated,

View File

@ -312,17 +312,6 @@ type DuoConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: sentinel:"-"
IntegrationKey string `protobuf:"bytes,1,opt,name=integration_key,json=integrationKey,proto3" json:"integration_key,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
SecretKey string `protobuf:"bytes,2,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
APIHostname string `protobuf:"bytes,3,opt,name=api_hostname,json=apiHostname,proto3" json:"api_hostname,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
PushInfo string `protobuf:"bytes,4,opt,name=push_info,json=pushInfo,proto3" json:"push_info,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
UsePasscode bool `protobuf:"varint,5,opt,name=use_passcode,json=usePasscode,proto3" json:"use_passcode,omitempty" sentinel:"-"`
}
func (x *DuoConfig) Reset() {
@ -357,58 +346,12 @@ func (*DuoConfig) Descriptor() ([]byte, []int) {
return file_helper_identity_mfa_types_proto_rawDescGZIP(), []int{2}
}
func (x *DuoConfig) GetIntegrationKey() string {
if x != nil {
return x.IntegrationKey
}
return ""
}
func (x *DuoConfig) GetSecretKey() string {
if x != nil {
return x.SecretKey
}
return ""
}
func (x *DuoConfig) GetAPIHostname() string {
if x != nil {
return x.APIHostname
}
return ""
}
func (x *DuoConfig) GetPushInfo() string {
if x != nil {
return x.PushInfo
}
return ""
}
func (x *DuoConfig) GetUsePasscode() bool {
if x != nil {
return x.UsePasscode
}
return false
}
// OktaConfig contains Okta configuration parameters required to perform Okta
// authentication.
type OktaConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: sentinel:"-"
OrgName string `protobuf:"bytes,1,opt,name=org_name,json=orgName,proto3" json:"org_name,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
APIToken string `protobuf:"bytes,2,opt,name=api_token,json=apiToken,proto3" json:"api_token,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
Production bool `protobuf:"varint,3,opt,name=production,proto3" json:"production,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
BaseURL string `protobuf:"bytes,4,opt,name=base_url,json=baseUrl,proto3" json:"base_url,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
PrimaryEmail bool `protobuf:"varint,5,opt,name=primary_email,json=primaryEmail,proto3" json:"primary_email,omitempty" sentinel:"-"`
}
func (x *OktaConfig) Reset() {
@ -443,61 +386,11 @@ func (*OktaConfig) Descriptor() ([]byte, []int) {
return file_helper_identity_mfa_types_proto_rawDescGZIP(), []int{3}
}
func (x *OktaConfig) GetOrgName() string {
if x != nil {
return x.OrgName
}
return ""
}
func (x *OktaConfig) GetAPIToken() string {
if x != nil {
return x.APIToken
}
return ""
}
func (x *OktaConfig) GetProduction() bool {
if x != nil {
return x.Production
}
return false
}
func (x *OktaConfig) GetBaseURL() string {
if x != nil {
return x.BaseURL
}
return ""
}
func (x *OktaConfig) GetPrimaryEmail() bool {
if x != nil {
return x.PrimaryEmail
}
return false
}
// PingIDConfig contains PingID configuration information
type PingIDConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: sentinel:"-"
UseBase64Key string `protobuf:"bytes,1,opt,name=use_base64_key,json=useBase64Key,proto3" json:"use_base64_key,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
UseSignature bool `protobuf:"varint,2,opt,name=use_signature,json=useSignature,proto3" json:"use_signature,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
Token string `protobuf:"bytes,3,opt,name=token,proto3" json:"token,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
IDPURL string `protobuf:"bytes,4,opt,name=idp_url,json=idpUrl,proto3" json:"idp_url,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
OrgAlias string `protobuf:"bytes,5,opt,name=org_alias,json=orgAlias,proto3" json:"org_alias,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
AdminURL string `protobuf:"bytes,6,opt,name=admin_url,json=adminUrl,proto3" json:"admin_url,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-"
AuthenticatorURL string `protobuf:"bytes,7,opt,name=authenticator_url,json=authenticatorUrl,proto3" json:"authenticator_url,omitempty" sentinel:"-"`
}
func (x *PingIDConfig) Reset() {
@ -532,55 +425,6 @@ func (*PingIDConfig) Descriptor() ([]byte, []int) {
return file_helper_identity_mfa_types_proto_rawDescGZIP(), []int{4}
}
func (x *PingIDConfig) GetUseBase64Key() string {
if x != nil {
return x.UseBase64Key
}
return ""
}
func (x *PingIDConfig) GetUseSignature() bool {
if x != nil {
return x.UseSignature
}
return false
}
func (x *PingIDConfig) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
func (x *PingIDConfig) GetIDPURL() string {
if x != nil {
return x.IDPURL
}
return ""
}
func (x *PingIDConfig) GetOrgAlias() string {
if x != nil {
return x.OrgAlias
}
return ""
}
func (x *PingIDConfig) GetAdminURL() string {
if x != nil {
return x.AdminURL
}
return ""
}
func (x *PingIDConfig) GetAuthenticatorURL() string {
if x != nil {
return x.AuthenticatorURL
}
return ""
}
// Secret represents all the types of secrets which the entity can hold.
// Each MFA type should add a secret type to the oneof block in this message.
type Secret struct {
@ -925,88 +769,54 @@ var file_helper_identity_mfa_types_proto_rawDesc = []byte{
0x71, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x61,
0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74,
0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x69,
0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x22, 0xb6,
0x01, 0x0a, 0x09, 0x44, 0x75, 0x6f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f,
0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f,
0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65,
0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x70, 0x69, 0x5f, 0x68, 0x6f, 0x73, 0x74,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x48,
0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x73, 0x68, 0x5f,
0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x73, 0x68,
0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x50,
0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x0a, 0x4f, 0x6b, 0x74, 0x61,
0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d,
0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e,
0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x28, 0x08, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19,
0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69,
0x6d, 0x61, 0x72, 0x79, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08,
0x52, 0x0c, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xef,
0x01, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x49, 0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x24, 0x0a, 0x0e, 0x75, 0x73, 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x36, 0x34, 0x5f, 0x6b, 0x65,
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x42, 0x61, 0x73, 0x65,
0x36, 0x34, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x69, 0x67,
0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73,
0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x12, 0x17, 0x0a, 0x07, 0x69, 0x64, 0x70, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x69, 0x64, 0x70, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x72, 0x67,
0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x72,
0x67, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f,
0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e,
0x55, 0x72, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10,
0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x72, 0x6c,
0x22, 0x66, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65,
0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x0b, 0x74,
0x6f, 0x74, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x0f, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65,
0x74, 0x48, 0x00, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42,
0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd6, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54,
0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65,
0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12,
0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72,
0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f,
0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18,
0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x12, 0x0a,
0x04, 0x73, 0x6b, 0x65, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x6b, 0x65,
0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c,
0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12,
0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
0x79, 0x22, 0xc1, 0x02, 0x0a, 0x14, 0x4d, 0x46, 0x41, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21,
0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49,
0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66, 0x61, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f,
0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x4d, 0x65,
0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x5f,
0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73,
0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68,
0x6f, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61,
0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73,
0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68,
0x6f, 0x64, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74,
0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20,
0x03, 0x28, 0x09, 0x52, 0x10, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f,
0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
0x79, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03,
0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69,
0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28,
0x09, 0x52, 0x02, 0x69, 0x64, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61,
0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74,
0x69, 0x74, 0x79, 0x2f, 0x6d, 0x66, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x22, 0x0b,
0x0a, 0x09, 0x44, 0x75, 0x6f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x0c, 0x0a, 0x0a, 0x4f,
0x6b, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x0e, 0x0a, 0x0c, 0x50, 0x69, 0x6e,
0x67, 0x49, 0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x66, 0x0a, 0x06, 0x53, 0x65, 0x63,
0x72, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x70, 0x5f, 0x73, 0x65, 0x63,
0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x66, 0x61, 0x2e,
0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x74, 0x6f,
0x74, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x22, 0xd6, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74,
0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69,
0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64,
0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20,
0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x16,
0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06,
0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x18, 0x05,
0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65,
0x79, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65,
0x79, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xc1, 0x02, 0x0a, 0x14, 0x4d,
0x46, 0x41, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73,
0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e,
0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66,
0x61, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03,
0x28, 0x09, 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x73,
0x12, 0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f,
0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52,
0x13, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73,
0x73, 0x6f, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74,
0x68, 0x6f, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52,
0x0f, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, 0x79, 0x70, 0x65, 0x73,
0x12, 0x2c, 0x0a, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f,
0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x69, 0x64,
0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x2e,
0x0a, 0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74,
0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65,
0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x42, 0x30,
0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73,
0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c,
0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2f, 0x6d, 0x66, 0x61,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View File

@ -60,49 +60,15 @@ message TOTPConfig {
// DuoConfig represents the configuration information required to perform
// Duo authentication.
message DuoConfig {
// @inject_tag: sentinel:"-"
string integration_key = 1;
// @inject_tag: sentinel:"-"
string secret_key = 2;
// @inject_tag: sentinel:"-"
string api_hostname = 3;
// @inject_tag: sentinel:"-"
string push_info = 4;
// @inject_tag: sentinel:"-"
bool use_passcode = 5;
}
// OktaConfig contains Okta configuration parameters required to perform Okta
// authentication.
message OktaConfig {
// @inject_tag: sentinel:"-"
string org_name = 1;
// @inject_tag: sentinel:"-"
string api_token = 2;
// @inject_tag: sentinel:"-"
bool production = 3;
// @inject_tag: sentinel:"-"
string base_url = 4;
// @inject_tag: sentinel:"-"
bool primary_email = 5;
}
// PingIDConfig contains PingID configuration information
message PingIDConfig {
// @inject_tag: sentinel:"-"
string use_base64_key = 1;
// @inject_tag: sentinel:"-"
bool use_signature = 2;
// @inject_tag: sentinel:"-"
string token = 3;
// @inject_tag: sentinel:"-"
string idp_url = 4;
// @inject_tag: sentinel:"-"
string org_alias = 5;
// @inject_tag: sentinel:"-"
string admin_url = 6;
// @inject_tag: sentinel:"-"
string authenticator_url = 7;
}
// Secret represents all the types of secrets which the entity can hold.

View File

@ -59,7 +59,6 @@ vault auth enable "jwt"
vault auth enable "kerberos"
vault auth enable "kubernetes"
vault auth enable "ldap"
vault auth enable "okta"
vault auth enable "radius"
vault auth enable "userpass"

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import AuthConfig from './_base';
export default AuthConfig.extend();

View File

@ -124,8 +124,6 @@ export default ApplicationAdapter.extend({
};
} else if (backend === 'jwt' || backend === 'oidc') {
options.data = { role, jwt };
} else if (backend === 'okta') {
options.data = { password, nonce };
} else {
options.data = token ? { token, password } : { password };
}
@ -140,13 +138,7 @@ export default ApplicationAdapter.extend({
mfa_payload: mfa_constraints.reduce((obj, { selectedMethod, passcode }) => {
let payload = [];
if (passcode) {
// duo requires passcode= prepended to the actual passcode
// this isn't a great UX so we add it behind the scenes to fulfill the requirement
// check if user added passcode= to avoid duplication
payload =
selectedMethod.type === 'duo' && !passcode.includes('passcode=')
? [`passcode=${passcode}`]
: [passcode];
payload = [passcode];
}
obj[selectedMethod.id] = payload;
return obj;
@ -173,7 +165,6 @@ export default ApplicationAdapter.extend({
oidc: 'login',
userpass: `login/${encodeURIComponent(username)}`,
ldap: `login/${encodeURIComponent(username)}`,
okta: `login/${encodeURIComponent(username)}`,
radius: `login/${encodeURIComponent(username)}`,
token: 'lookup-self',
};

View File

@ -30,10 +30,6 @@ const BACKENDS = supportedAuthBackends();
* @param {string} namespace- The currently active namespace.
* @param {string} selectedAuth - The auth method that is currently selected in the dropdown.
* @param {function} onSuccess - Fired on auth success.
* @param {function} [setOktaNumberChallenge] - Sets whether we are waiting for okta number challenge to be used to sign in.
* @param {boolean} [waitingForOktaNumberChallenge=false] - Determines if we are waiting for the Okta Number Challenge to sign in.
* @param {function} [setCancellingAuth] - Sets whether we are cancelling or not the login authentication for Okta Number Challenge.
* @param {boolean} [cancelAuthForOktaNumberChallenge=false] - Determines if we are cancelling the login authentication for the Okta Number Challenge.
*/
const DEFAULTS = {
@ -60,9 +56,6 @@ export default Component.extend(DEFAULTS, {
oldNamespace: null,
authMethods: BACKENDS,
// number answer for okta number challenge if applicable
oktaNumberChallengeAnswer: null,
didReceiveAttrs() {
this._super(...arguments);
const {
@ -72,13 +65,10 @@ export default Component.extend(DEFAULTS, {
namespace: ns,
selectedAuth: newMethod,
oldSelectedAuth: oldMethod,
cancelAuthForOktaNumberChallenge: cancelAuth,
} = this;
// if we are cancelling the login then we reset the number challenge answer and cancel the current authenticate and polling tasks
if (cancelAuth) {
this.set('oktaNumberChallengeAnswer', null);
this.authenticate.cancelAll();
this.pollForOktaNumberChallenge.cancelAll();
}
next(() => {
if (!token && (oldNS === null || oldNS !== ns)) {
@ -237,11 +227,7 @@ export default Component.extend(DEFAULTS, {
cluster: { id: clusterId },
} = this;
try {
if (backendType === 'okta') {
this.pollForOktaNumberChallenge.perform(data.nonce, data.path);
} else {
this.delayAuthMessageReminder.perform();
}
this.delayAuthMessageReminder.perform();
const authResponse = yield this.auth.authenticate({
clusterId,
backend: backendType,
@ -258,28 +244,6 @@ export default Component.extend(DEFAULTS, {
})
),
pollForOktaNumberChallenge: task(function* (nonce, mount) {
// yield for 1s to wait to see if there is a login error before polling
yield timeout(1000);
if (this.error) {
return;
}
let response = null;
this.setOktaNumberChallenge(true);
this.setCancellingAuth(false);
// keep polling /auth/okta/verify/:nonce API every 1s until a response is given with the correct number for the Okta Number Challenge
while (response === null) {
// when testing, the polling loop causes promises to be rejected making acceptance tests fail
// so disable the poll in tests
if (Ember.testing) {
return;
}
yield timeout(1000);
response = yield this.auth.getOktaNumberChallengeAnswer(nonce, mount);
}
this.set('oktaNumberChallengeAnswer', response);
}),
delayAuthMessageReminder: task(function* () {
if (Ember.testing) {
yield timeout(0);
@ -311,14 +275,6 @@ export default Component.extend(DEFAULTS, {
if (this.customPath || backend.id) {
data.path = this.customPath || backend.id;
}
// add nonce field for okta backend
if (backend.type === 'okta') {
data.nonce = uuidv4();
// add a default path of okta if it doesn't exist to be used for Okta Number Challenge
if (!data.path) {
data.path = 'okta';
}
}
return this.authenticate.unlinked().perform(backend.type, data);
},
handleError(e) {
@ -327,9 +283,5 @@ export default Component.extend(DEFAULTS, {
error: e ? this.auth.handleError(e) : null,
});
},
returnToLoginFromOktaNumberChallenge() {
this.setOktaNumberChallenge(false);
this.set('oktaNumberChallengeAnswer', null);
},
},
});

View File

@ -16,7 +16,7 @@ export default class MfaMethodCreateController extends Controller {
@service router;
queryParams = ['type'];
methodNames = ['TOTP', 'Duo', 'Okta', 'PingID'];
methodNames = ['TOTP'];
@tracked type = null;
@tracked method = null;

View File

@ -90,7 +90,6 @@ export default Controller.extend({
},
cancelAuthentication() {
this.set('cancelAuth', true);
this.set('waitingForOktaNumberChallenge', false);
},
},
});

View File

@ -53,13 +53,6 @@ const MOUNTABLE_AUTH_METHODS = [
glyph: 'auth',
category: 'infra',
},
{
displayName: 'Okta',
value: 'okta',
type: 'okta',
category: 'infra',
glyph: 'okta-color',
},
{
displayName: 'RADIUS',
value: 'radius',

View File

@ -36,14 +36,6 @@ const SUPPORTED_AUTH_BACKENDS = [
displayNamePath: 'metadata.username',
formAttributes: ['username', 'password'],
},
{
type: 'okta',
typeDisplay: 'Okta',
description: 'Authenticate with your Okta username and password.',
tokenPath: 'client_token',
displayNamePath: 'metadata.username',
formAttributes: ['username', 'password'],
},
{
type: 'jwt',
typeDisplay: 'JWT',

View File

@ -7,7 +7,7 @@ import { helper as buildHelper } from '@ember/component/helper';
// The UI supports management of these auth methods (i.e. configuring roles or users)
// otherwise only configuration of the method is supported.
const MANAGED_AUTH_BACKENDS = ['cert', 'kubernetes', 'ldap', 'okta', 'radius', 'userpass'];
const MANAGED_AUTH_BACKENDS = ['cert', 'kubernetes', 'ldap', 'radius', 'userpass'];
export function supportedManagedAuthBackends() {
return MANAGED_AUTH_BACKENDS;

View File

@ -38,12 +38,6 @@ const TABS_FOR_SETTINGS = {
routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'],
},
],
okta: [
{
label: 'Configuration',
routeParams: ['vault.cluster.settings.auth.configure.section', 'configuration'],
},
],
radius: [
{
label: 'Configuration',

View File

@ -1,46 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import AuthConfig from '../auth-config';
import fieldToAttrs from 'vault/utils/field-to-attrs';
import { combineFieldGroups } from 'vault/utils/openapi-to-attrs';
export default AuthConfig.extend({
useOpenAPI: true,
orgName: attr('string', {
helpText: 'Name of the organization to be used in the Okta API',
}),
apiToken: attr('string', {
helpText:
'Okta API token. This is required to query Okta for user group membership. If this is not supplied only locally configured groups will be enabled.',
}),
baseUrl: attr('string', {
helpText:
'If set, will be used as the base domain for API requests. Examples are okta.com, oktapreview.com, and okta-emea.com',
}),
bypassOktaMfa: attr('boolean', {
defaultValue: false,
helpText:
"Useful if using Vault's built-in MFA mechanisms. Will also cause certain other statuses to be ignored, such as PASSWORD_EXPIRED",
}),
fieldGroups: computed('newFields', function () {
let groups = [
{
default: ['orgName'],
},
{
Options: ['apiToken', 'baseUrl', 'bypassOktaMfa'],
},
];
if (this.newFields) {
groups = combineFieldGroups(groups, this.newFields, []);
}
return fieldToAttrs(this, groups);
}),
});

View File

@ -11,25 +11,11 @@ import { isPresent } from '@ember/utils';
const METHOD_PROPS = {
common: [],
duo: ['username_format', 'secret_key', 'integration_key', 'api_hostname', 'push_info', 'use_passcode'],
okta: ['username_format', 'mount_accessor', 'org_name', 'api_token', 'base_url', 'primary_email'],
totp: ['issuer', 'period', 'key_size', 'qr_size', 'algorithm', 'digits', 'skew', 'max_validation_attempts'],
pingid: [
'username_format',
'settings_file_base64',
'use_signature',
'idp_url',
'admin_url',
'authenticator_url',
'org_alias',
],
};
const REQUIRED_PROPS = {
duo: ['secret_key', 'integration_key', 'api_hostname'],
okta: ['org_name', 'api_token'],
totp: ['issuer'],
pingid: ['settings_file_base64'],
};
const validators = Object.keys(REQUIRED_PROPS).reduce((obj, type) => {
@ -61,62 +47,6 @@ export default class MfaMethod extends Model {
namespace_id;
@attr('string') mount_accessor;
// PING ID
@attr('string', {
label: 'Settings file',
subText: 'A base-64 encoded third party setting file retrieved from the PingIDs configuration page.',
})
settings_file_base64;
@attr('boolean') use_signature;
@attr('string') idp_url;
@attr('string') admin_url;
@attr('string') authenticator_url;
@attr('string') org_alias;
// OKTA
@attr('string', {
label: 'Organization name',
subText: 'Name of the organization to be used in the Okta API.',
})
org_name;
@attr('string', {
label: 'Okta API key',
})
api_token;
@attr('string', {
label: 'Base URL',
subText:
'If set, will be used as the base domain for API requests. Example are okta.com, oktapreview.com and okta-emea.com.',
})
base_url;
@attr('boolean') primary_email;
// DUO
@attr('string', {
label: 'Duo secret key',
sensitive: true,
})
secret_key;
@attr('string', {
label: 'Duo integration key',
sensitive: true,
})
integration_key;
@attr('string', {
label: 'Duo API hostname',
})
api_hostname;
@attr('string', {
label: 'Duo push information',
subText: 'Additional information displayed to the user when the push is presented to them.',
})
push_info;
@attr('boolean', {
label: 'Passcode reminder',
subText: 'If this is turned on, the user is reminded to use the passcode upon MFA validation.',
})
use_passcode;
// TOTP
@attr('string', {
label: 'Issuer',

View File

@ -22,7 +22,6 @@ export default Route.extend(UnloadModelRoute, {
'oidc-configuration': 'auth-config/oidc',
'kubernetes-configuration': 'auth-config/kubernetes',
'ldap-configuration': 'auth-config/ldap',
'okta-configuration': 'auth-config/okta',
'radius-configuration': 'auth-config/radius',
};
return MODELS[`${backendType}-${section}`];

View File

@ -482,20 +482,4 @@ export default Service.extend({
this.set('tokens', tokenNames);
},
getOktaNumberChallengeAnswer(nonce, mount) {
const url = `/v1/auth/${mount}/verify/${nonce}`;
return this.ajax(url, 'GET', {}).then(
(resp) => {
return resp.data.correct_answer;
},
(e) => {
// if error status is 404, return and keep polling for a response
if (e.status === 404) {
return null;
} else {
throw e;
}
}
);
},
});

View File

@ -4,13 +4,6 @@
~}}
<div class="auth-form" data-test-auth-form>
{{#if (and this.waitingForOktaNumberChallenge (not this.cancelAuthForOktaNumberChallenge))}}
<OktaNumberChallenge
@correctAnswer={{this.oktaNumberChallengeAnswer}}
@hasError={{(not-eq this.error null)}}
@onReturnToLogin={{action "returnToLoginFromOktaNumberChallenge"}}
/>
{{else}}
{{#if this.hasMethodsWithPath}}
<nav class="tabs is-marginless">
<ul>
@ -184,5 +177,4 @@
</form>
{{/if}}
</div>
{{/if}}
</div>

View File

@ -1,43 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<div class="auth-form" data-test-okta-number-challenge>
<div class="box is-marginless is-shadowless">
<div class="field has-top-margin-xs">
<p data-test-okta-number-challenge-description>
To finish signing in, you will need to complete an additional MFA step.</p>
{{#if @hasError}}
<div class="has-top-margin-s">
<MessageError @errorMessage="There was a problem" />
<button
type="button"
class="button"
{{on "click" @onReturnToLogin}}
data-test-return-from-okta-number-challenge
>Return to login</button>
</div>
{{else if @correctAnswer}}
<div class="has-top-margin-s">
<p class="has-text-black has-text-weight-semibold" data-test-okta-number-challenge-verification-type>Okta
verification</p>
<p data-test-okta-number-challenge-verification-description>Select the following number to complete verification:</p>
<h1
class="title has-font-weight-normal has-top-margin-m has-bottom-margin-s"
data-test-okta-number-challenge-answer
>{{@correctAnswer}}</h1>
</div>
{{else}}
<div class="has-top-margin-l has-bottom-margin-m">
<div class="is-flex-row">
<FlightIcon @name="loading" />
<div class="has-left-margin-xs">
<p data-test-okta-number-challenge-loading>Please wait...</p>
</div>
</div>
</div>
{{/if}}
</div>
</div>
</div>

View File

@ -1,16 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<WizardSection
@headerText="Okta"
@headerIcon="okta-color"
@docText="Docs: Okta Authentication"
@docPath="/docs/auth/okta.html"
>
<p>
The Okta Auth Method allows authentication using Okta and user/password credentials. This allows Vault to be integrated
into environments using Okta.
</p>
</WizardSection>

View File

@ -32,7 +32,7 @@
<div class="box is-fullwidth is-shadowless">
<p>
<b>Step 1:</b>
Set up an MFA configuration using one of the methods; TOTP, Okta, Duo or Pingid.
Set up an MFA configuration using TOTP method.
</p>
<p>
<b>Step 2:</b>

View File

@ -67,11 +67,11 @@
<div class="radio-card-row is-flex-v-centered">
<div>
<Icon
@name={{if (eq methodName "Okta") "okta-color" (lowercase methodName)}}
@name={{lowercase methodName}}
@size="24"
class={{if (eq methodName "TOTP") "has-text-grey"}}
/>
<p class="has-text-weight-semibold has-text-center {{if (eq methodName 'Okta') 'has-top-margin-xs'}}">
<p class="has-text-weight-semibold has-text-center">
{{methodName}}
</p>
</div>

View File

@ -37,13 +37,9 @@
<button type="button" class="icon-button" {{on "click" (fn (mut this.mfaAuthData) null)}}>
<Icon @name="arrow-left" @size="24" aria-label="Back to login" class="icon-blue" />
</button>
{{else if this.waitingForOktaNumberChallenge}}
<button type="button" class="icon-button" {{on "click" (action "cancelAuthentication")}}>
<Icon @name="arrow-left" @size="24" aria-label="Back to login" class="icon-blue" />
</button>
{{/if}}
<h1 class="title is-3">
{{if (or this.mfaAuthData this.waitingForOktaNumberChallenge) "Authenticate" "Sign in to Vault"}}
{{if this.mfaAuthData "Authenticate" "Sign in to Vault"}}
</h1>
</div>
{{/if}}
@ -120,19 +116,6 @@
@onSuccess={{action "onMfaSuccess"}}
@onError={{fn (mut this.mfaErrors)}}
/>
{{else}}
<AuthForm
@wrappedToken={{this.wrappedToken}}
@cluster={{this.model}}
@namespace={{this.namespaceQueryParam}}
@redirectTo={{this.redirectTo}}
@selectedAuth={{this.authMethod}}
@onSuccess={{action "onAuthResponse"}}
@setOktaNumberChallenge={{fn (mut this.waitingForOktaNumberChallenge)}}
@waitingForOktaNumberChallenge={{this.waitingForOktaNumberChallenge}}
@setCancellingAuth={{fn (mut this.cancelAuth)}}
@cancelAuthForOktaNumberChallenge={{this.cancelAuth}}
/>
{{/if}}
</Page.content>
<Page.footer>

View File

@ -21,12 +21,9 @@ export const localIconMap = {
kmip: 'unlock',
kv: 'key-values',
ldap: 'user',
okta: 'okta-color',
radius: 'user',
ssh: 'terminal-screen',
totp: 'history',
duo: null,
pingid: null,
transit: 'swap-horizontal',
userpass: 'identity-user',
stopwatch: 'clock',
@ -138,8 +135,6 @@ export const structureIconMap = {
'logo-kubernetes-monochrome': 'kubernetes',
'logo-microsoft-color': 'microsoft-color',
'logo-microsoft-monochrome': 'microsoft',
'logo-okta-color': 'okta-color',
'logo-okta-monochrome': 'okta',
'logo-slack-color': 'slack-color',
'logo-slack-monochrome': 'slack',
'logo-vmware-color': 'vmware-color',

View File

@ -1,24 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'ember-cli-mirage';
export default Factory.extend({
api_hostname: 'api-foobar.duosecurity.com',
mount_accessor: '',
name: '', // returned but cannot be set at this time
namespace_id: 'root',
pushinfo: '',
type: 'duo',
use_passcode: false,
username_template: '',
afterCreate(record) {
if (record.name) {
console.warn('Endpoint ignored these unrecognized parameters: [name]'); // eslint-disable-line
record.name = '';
}
},
});

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'ember-cli-mirage';
export default Factory.extend({
base_url: 'okta.com',
mount_accessor: '',
name: '', // returned but cannot be set at this time
namespace_id: 'root',
org_name: 'dev-foobar',
type: 'okta',
username_template: '', // returned but cannot be set at this time
afterCreate(record) {
if (record.name) {
console.warn('Endpoint ignored these unrecognized parameters: [name]'); // eslint-disable-line
record.name = '';
}
},
});

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { Factory } from 'ember-cli-mirage';
export default Factory.extend({
use_signature: true,
idp_url: 'https://foobar.pingidentity.com/pingid',
admin_url: 'https://foobar.pingidentity.com/pingid',
authenticator_url: 'https://authenticator.pingone.com/pingid/ppm',
org_alias: 'foobarbaz',
type: 'pingid',
username_template: '',
namespace_id: 'root',
});

View File

@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M23.988 11.994C23.988 18.618 18.618 23.988 11.994 23.988C5.37 23.988 0 18.618 0 11.994C0 5.37 5.37 0 11.994 0C18.618 0 23.988 5.37 23.988 11.994Z" fill="#59B734"/>
<path d="M20.4045 11.9505C20.3153 13.3849 19.1259 14.5026 17.6888 14.5026C16.2516 14.5026 15.0622 13.3849 14.973 11.9505H20.4045ZM9.01951 11.9505C8.93647 13.3893 7.7457 14.5136 6.30451 14.514H3.58276V11.9505H9.01876H9.01951ZM14.7173 9.07275V11.793C14.7173 11.8462 14.715 11.898 14.712 11.9505V14.5095H12.153V9.072H14.7173V9.07275Z" fill="#E6F3D8"/>
<path d="M11.8395 9.07275V14.5095C10.4 14.4264 9.27525 13.2349 9.27525 11.793V9.07275H11.8402H11.8395ZM17.6887 9.07275C19.1301 9.07311 20.3211 10.1973 20.4045 11.6362H14.973C15.0564 10.1976 16.247 9.07351 17.688 9.07275H17.6887ZM6.30375 9.07275C7.74507 9.07311 8.93607 10.1973 9.0195 11.6362H3.5835V9.07275H6.30375Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 957 B

View File

@ -1 +0,0 @@
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><path d="M6.038 19.055a6.038 6.038 0 1 0 0 12.075 6.038 6.038 0 0 0 0-12.075zm0 9.057a3.019 3.019 0 1 1 0-6.038 3.019 3.019 0 0 1 0 6.038zm10.601-.995v3.502c0 .233-.203.458-.516.458h-2.017c-.231 0-.523-.096-.523-.446V15.438c0-.205.171-.438.521-.438h2.02c.23 0 .517.13.517.48l.002 6.846c0 .49.6.726.933.367l3.212-3.46c.037-.036.093-.094.239-.138a.856.856 0 0 1 .235-.02h2.467c.503 0 .651.56.419.844l-3.586 3.983c-.586.62-.641.859-.128 1.472l.235.245 4.512 4.626c.232.285.081.851-.408.851l-2.724.001c-.098 0-.194-.006-.258-.025-.146-.043-.178-.102-.216-.14-.016-.014-2.516-2.647-4.023-4.188a.533.533 0 0 0-.913.373zm16.472 3.437a.508.508 0 0 1-.438.556 6.04 6.04 0 0 1-6.845-5.817v-9.829c0-.31.242-.464.496-.464h2.009c.383 0 .522.289.522.476l.005 3.055c0 .314.256.545.57.545h2.721c.213 0 .451.204.451.57v1.913a.495.495 0 0 1-.474.522l-2.701-.002c-.323 0-.56.26-.56.583v2.464c0 .054-.007.109-.004.162a3.011 3.011 0 0 0 3.473 2.82.484.484 0 0 1 .56.425l.215 2.02zM47.65 28.02c.2 0 .35.201.35.401v2.147c0 .603-1.29.562-1.534.56-1.424-.01-2.336-.585-2.905-1.449a6.037 6.037 0 1 1-3.921-10.624c1.083 0 2.098.288 2.977.787l-.001-.256c0-.31.27-.511.524-.511h2.013c.383 0 .523.324.523.511l.004 5.504v.022c.005 2.3.26 2.908 1.97 2.908zm-8.008.09a3.019 3.019 0 1 0 0-6.038 3.019 3.019 0 0 0 0 6.037z" fill="#007DC1" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.49063 2 2 6.45844 2 12C2 17.5416 6.45875 22 12 22C17.5413 22 22 17.5413 22 12C22 6.45875 17.5094 2 12 2ZM12 17C9.22937 17 7 14.7706 7 12C7 9.22937 9.22937 7 12 7C14.7706 7 17 9.22937 17 12C17 14.7706 14.7706 17 12 17Z" fill="#007DC1"/>
</svg>

Before

Width:  |  Height:  |  Size: 356 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1,74 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | okta-number-challenge', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
this.oktaNumberChallengeAnswer = null;
this.hasError = false;
});
test('it should render correct descriptions', async function (assert) {
await render(hbs`<OktaNumberChallenge @correctAnswer={{this.oktaNumberChallengeAnswer}}/>`);
assert
.dom('[data-test-okta-number-challenge-description]')
.includesText(
'To finish signing in, you will need to complete an additional MFA step.',
'Correct description renders'
);
assert
.dom('[data-test-okta-number-challenge-loading]')
.includesText('Please wait...', 'Correct loading description renders');
});
test('it should show correct number for okta number challenge', async function (assert) {
this.set('oktaNumberChallengeAnswer', 1);
await render(hbs`<OktaNumberChallenge @correctAnswer={{this.oktaNumberChallengeAnswer}}/>`);
assert
.dom('[data-test-okta-number-challenge-description]')
.includesText(
'To finish signing in, you will need to complete an additional MFA step.',
'Correct description renders'
);
assert
.dom('[data-test-okta-number-challenge-verification-type]')
.includesText('Okta verification', 'Correct verification type renders');
assert
.dom('[data-test-okta-number-challenge-verification-description]')
.includesText(
'Select the following number to complete verification:',
'Correct verification description renders'
);
assert
.dom('[data-test-okta-number-challenge-answer]')
.includesText('1', 'Correct okta number challenge answer renders');
});
test('it should show error screen', async function (assert) {
this.set('hasError', true);
await render(
hbs`<OktaNumberChallenge @correctAnswer={{this.oktaNumberChallengeAnswer}} @hasError={{this.hasError}} @onReturnToLogin={{fn (mut this.returnToLogin) true}}/>`
);
assert
.dom('[data-test-okta-number-challenge-description]')
.includesText(
'To finish signing in, you will need to complete an additional MFA step.',
'Correct description renders'
);
assert
.dom('[data-test-error]')
.includesText('There was a problem', 'Displays error that there was a problem');
await click('[data-test-return-from-okta-number-challenge]');
assert.true(this.returnToLogin, 'onReturnToLogin was triggered');
});
});

View File

@ -1,292 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package identity
import (
"context"
"fmt"
"net/http"
"reflect"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/credential/userpass"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)
var identityMFACoreConfigDUO = &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
}
var (
secret_key = "<secret key for DUO>"
integration_key = "<integration key>"
api_hostname = "<api hostname>"
)
func TestInteg_PolicyMFADUO(t *testing.T) {
t.Skip("This test requires manual intervention and DUO verify on cellphone is needed")
cluster := vault.NewTestCluster(t, identityMFACoreConfigDUO, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
// Enable Userpass authentication
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatalf("failed to enable userpass auth: %v", err)
}
err = mfaGeneratePolicyDUOTest(client)
if err != nil {
t.Fatalf("DUO verification failed")
}
}
func mfaGeneratePolicyDUOTest(client *api.Client) error {
var err error
rules := `
path "secret/foo" {
capabilities = ["read"]
mfa_methods = ["my_duo"]
}
`
auths, err := client.Sys().ListAuth()
if err != nil {
return fmt.Errorf("failed to list auth mount")
}
mountAccessor := auths["userpass/"].Accessor
err = client.Sys().PutPolicy("mfa_policy", rules)
if err != nil {
return fmt.Errorf("failed to create mfa_policy: %v", err)
}
_, err = client.Logical().Write("auth/userpass/users/vaultmfa", map[string]interface{}{
"password": "testpassword",
"policies": "mfa_policy",
})
if err != nil {
return fmt.Errorf("failed to configure userpass backend: %v", err)
}
secret, err := client.Logical().Write("auth/userpass/login/vaultmfa", map[string]interface{}{
"password": "testpassword",
})
if err != nil {
return fmt.Errorf("failed to login using userpass auth: %v", err)
}
userpassToken := secret.Auth.ClientToken
secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{
"token": userpassToken,
})
if err != nil {
return fmt.Errorf("failed to lookup userpass authenticated token: %v", err)
}
// entityID := secret.Data["entity_id"].(string)
mfaConfigData := map[string]interface{}{
"mount_accessor": mountAccessor,
"secret_key": secret_key,
"integration_key": integration_key,
"api_hostname": api_hostname,
}
_, err = client.Logical().Write("sys/mfa/method/duo/my_duo", mfaConfigData)
if err != nil {
return fmt.Errorf("failed to persist TOTP MFA configuration: %v", err)
}
// Write some data in the path that requires TOTP MFA
genericData := map[string]interface{}{
"somedata": "which can only be read if MFA succeeds",
}
_, err = client.Logical().Write("secret/foo", genericData)
if err != nil {
return fmt.Errorf("failed to store data in generic backend: %v", err)
}
// Replace the token in client with the one that has access to MFA
// required path
originalToken := client.Token()
defer client.SetToken(originalToken)
client.SetToken(userpassToken)
// Create a GET request and set the MFA header containing the generated
// TOTP passcode
secretRequest := client.NewRequest("GET", "/v1/secret/foo")
secretRequest.Headers = make(http.Header)
// mfaHeaderValue := "my_duo:" + totpPasscode
// secretRequest.Headers.Add("X-Vault-MFA", mfaHeaderValue)
// Make the request
resp, err := client.RawRequest(secretRequest)
if resp != nil {
defer resp.Body.Close()
}
if resp != nil && resp.StatusCode == 403 {
return fmt.Errorf("failed to read the secret")
}
if err != nil {
return fmt.Errorf("failed to read the secret: %v", err)
}
// It should be possible to access the secret
secret, err = api.ParseSecret(resp.Body)
if err != nil {
return fmt.Errorf("failed to parse the secret: %v", err)
}
if !reflect.DeepEqual(secret.Data, genericData) {
return fmt.Errorf("bad: generic data; expected: %#v\nactual: %#v", genericData, secret.Data)
}
return nil
}
func TestInteg_LoginMFADUO(t *testing.T) {
t.Skip("This test requires manual intervention and DUO verify on cellphone is needed")
cluster := vault.NewTestCluster(t, identityMFACoreConfigDUO, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
// Enable Userpass authentication
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatalf("failed to enable userpass auth: %v", err)
}
err = mfaGenerateLoginDUOTest(client)
if err != nil {
t.Fatalf("DUO verification failed. error: %s", err)
}
}
func mfaGenerateLoginDUOTest(client *api.Client) error {
var err error
auths, err := client.Sys().ListAuth()
if err != nil {
return fmt.Errorf("failed to list auth mount")
}
mountAccessor := auths["userpass/"].Accessor
_, err = client.Logical().Write("auth/userpass/users/vaultmfa", map[string]interface{}{
"password": "testpassword",
})
if err != nil {
return fmt.Errorf("failed to configure userpass backend: %v", err)
}
secret, err := client.Logical().Write("identity/entity", map[string]interface{}{
"name": "test",
})
if err != nil {
return fmt.Errorf("failed to create an entity")
}
entityID := secret.Data["id"].(string)
_, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{
"name": "vaultmfa",
"canonical_id": entityID,
"mount_accessor": mountAccessor,
})
if err != nil {
return fmt.Errorf("failed to create an entity alias")
}
var methodID string
// login MFA
{
// create a config
mfaConfigData := map[string]interface{}{
"username_format": fmt.Sprintf("{{identity.entity.aliases.%s.name}}", mountAccessor),
"secret_key": secret_key,
"integration_key": integration_key,
"api_hostname": api_hostname,
}
resp, err := client.Logical().Write("identity/mfa/method/duo", mfaConfigData)
if err != nil || (resp == nil) {
return fmt.Errorf("bad: resp: %#v\n err: %v", resp, err)
}
methodID = resp.Data["method_id"].(string)
if methodID == "" {
return fmt.Errorf("method ID is empty")
}
// creating MFAEnforcementConfig
_, err = client.Logical().Write("identity/mfa/login-enforcement/randomName", map[string]interface{}{
"auth_method_accessors": []string{mountAccessor},
"auth_method_types": []string{"userpass"},
"identity_entity_ids": []string{entityID},
"name": "randomName",
"mfa_method_ids": []string{methodID},
})
if err != nil {
return fmt.Errorf("failed to configure MFAEnforcementConfig: %v", err)
}
}
secret, err = client.Logical().Write("auth/userpass/login/vaultmfa", map[string]interface{}{
"password": "testpassword",
})
if err != nil {
return fmt.Errorf("failed to login using userpass auth: %v", err)
}
if secret.Auth == nil || secret.Auth.MFARequirement == nil {
return fmt.Errorf("two phase login returned nil MFARequirement")
}
if secret.Auth.MFARequirement.MFARequestID == "" {
return fmt.Errorf("MFARequirement contains empty MFARequestID")
}
if secret.Auth.MFARequirement.MFAConstraints == nil || len(secret.Auth.MFARequirement.MFAConstraints) == 0 {
return fmt.Errorf("MFAConstraints is nil or empty")
}
mfaConstraints, ok := secret.Auth.MFARequirement.MFAConstraints["randomName"]
if !ok {
return fmt.Errorf("failed to find the mfaConstrains")
}
if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 {
return fmt.Errorf("")
}
for _, mfaAny := range mfaConstraints.Any {
if mfaAny.ID != methodID || mfaAny.Type != "duo" {
return fmt.Errorf("invalid mfa constraints")
}
}
// validation
secret, err = client.Sys().MFAValidateWithContext(context.Background(),
secret.Auth.MFARequirement.MFARequestID,
map[string]interface{}{
methodID: []string{},
})
if err != nil {
return fmt.Errorf("MFA failed: %v", err)
}
if secret.Auth.ClientToken == "" {
return fmt.Errorf("MFA was not enforced")
}
return nil
}

View File

@ -1,358 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package identity
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/credential/okta"
"github.com/hashicorp/vault/builtin/credential/userpass"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)
var (
org_name = "<okta org name>"
api_token = "<okta api token>"
)
var identityOktaMFACoreConfig = &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
"okta": okta.Factory,
},
}
func TestOktaEngineMFA(t *testing.T) {
t.Skip("This test requires manual intervention and OKTA verify on cellphone is needed")
cluster := vault.NewTestCluster(t, identityOktaMFACoreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
// Enable Okta engine
err := client.Sys().EnableAuthWithOptions("okta", &api.EnableAuthOptions{
Type: "okta",
})
if err != nil {
t.Fatalf("failed to enable okta auth: %v", err)
}
_, err = client.Logical().Write("auth/okta/config", map[string]interface{}{
"base_url": "okta.com",
"org_name": org_name,
"api_token": api_token,
})
if err != nil {
t.Fatalf("error configuring okta mount: %v", err)
}
_, err = client.Logical().Write("auth/okta/groups/testgroup", map[string]interface{}{
"policies": "default",
})
if err != nil {
t.Fatalf("error configuring okta group, %v", err)
}
_, err = client.Logical().Write("auth/okta/login/<okta username>", map[string]interface{}{
"password": "<okta password>",
})
if err != nil {
t.Fatalf("error configuring okta group, %v", err)
}
}
func TestInteg_PolicyMFAOkta(t *testing.T) {
t.Skip("This test requires manual intervention and OKTA verify on cellphone is needed")
cluster := vault.NewTestCluster(t, identityOktaMFACoreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
// Enable Userpass authentication
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatalf("failed to enable userpass auth: %v", err)
}
err = mfaGenerateOktaPolicyMFATest(client)
if err != nil {
t.Fatalf("Okta failed: %s", err)
}
}
func mfaGenerateOktaPolicyMFATest(client *api.Client) error {
var err error
rules := `
path "secret/foo" {
capabilities = ["read"]
mfa_methods = ["my_okta"]
}
`
err = client.Sys().PutPolicy("mfa_policy", rules)
if err != nil {
return fmt.Errorf("failed to create mfa_policy: %v", err)
}
// listing auth mounts to find the mount accessor for the userpass
auths, err := client.Sys().ListAuth()
if err != nil {
return fmt.Errorf("error listing auth mounts")
}
mountAccessor := auths["userpass/"].Accessor
// creating a user in userpass
_, err = client.Logical().Write("auth/userpass/users/testuser", map[string]interface{}{
"password": "testpassword",
})
if err != nil {
return fmt.Errorf("failed to configure userpass backend: %v", err)
}
// creating an identity with email metadata to be used for MFA validation
secret, err := client.Logical().Write("identity/entity", map[string]interface{}{
"name": "test-entity",
"policies": "mfa_policy",
"metadata": map[string]string{
"email": "<okta username>",
},
})
if err != nil {
return fmt.Errorf("failed to create an entity")
}
entityID := secret.Data["id"].(string)
// assigning the entity ID to the testuser alias
_, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{
"name": "testuser",
"canonical_id": entityID,
"mount_accessor": mountAccessor,
})
if err != nil {
return fmt.Errorf("failed to create an entity alias")
}
mfaConfigData := map[string]interface{}{
"mount_accessor": mountAccessor,
"org_name": org_name,
"api_token": api_token,
"primary_email": true,
"username_format": "{{entity.metadata.email}}",
}
_, err = client.Logical().Write("sys/mfa/method/okta/my_okta", mfaConfigData)
if err != nil {
return fmt.Errorf("failed to persist TOTP MFA configuration: %v", err)
}
// Write some data in the path that requires TOTP MFA
genericData := map[string]interface{}{
"somedata": "which can only be read if MFA succeeds",
}
_, err = client.Logical().Write("secret/foo", genericData)
if err != nil {
return fmt.Errorf("failed to store data in generic backend: %v", err)
}
// Replace the token in client with the one that has access to MFA
// required path
originalToken := client.Token()
defer client.SetToken(originalToken)
// login to the testuser
secret, err = client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{
"password": "testpassword",
})
if err != nil {
return fmt.Errorf("failed to login using userpass auth: %v", err)
}
userpassToken := secret.Auth.ClientToken
client.SetToken(userpassToken)
secret, err = client.Logical().Read("secret/foo")
if err != nil {
return fmt.Errorf("failed to read the secret: %v", err)
}
// It should be possible to access the secret
// secret, err = api.ParseSecret(resp.Body)
if err != nil {
return fmt.Errorf("failed to parse the secret: %v", err)
}
if !reflect.DeepEqual(secret.Data, genericData) {
return fmt.Errorf("bad: generic data; expected: %#v\nactual: %#v", genericData, secret.Data)
}
return nil
}
func TestInteg_LoginMFAOkta(t *testing.T) {
t.Skip("This test requires manual intervention and OKTA verify on cellphone is needed")
cluster := vault.NewTestCluster(t, identityOktaMFACoreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
// Enable Userpass authentication
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatalf("failed to enable userpass auth: %v", err)
}
err = mfaGenerateOktaLoginMFATest(client)
if err != nil {
t.Fatalf("Okta failed: %s", err)
}
}
func mfaGenerateOktaLoginMFATest(client *api.Client) error {
var err error
auths, err := client.Sys().ListAuth()
if err != nil {
return fmt.Errorf("failed to list auth mounts")
}
mountAccessor := auths["userpass/"].Accessor
_, err = client.Logical().Write("auth/userpass/users/testuser", map[string]interface{}{
"password": "testpassword",
})
if err != nil {
return fmt.Errorf("failed to configure userpass backend: %v", err)
}
secret, err := client.Logical().Write("identity/entity", map[string]interface{}{
"name": "test-entity",
"metadata": map[string]string{
"email": "<okta username>",
},
})
if err != nil {
return fmt.Errorf("failed to create an entity")
}
entityID := secret.Data["id"].(string)
_, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{
"name": "testuser",
"canonical_id": entityID,
"mount_accessor": mountAccessor,
})
if err != nil {
return fmt.Errorf("failed to create an entity alias")
}
var methodID string
var userpassToken string
// login MFA
{
// create a config
mfaConfigData := map[string]interface{}{
"mount_accessor": mountAccessor,
"org_name": org_name,
"api_token": api_token,
"primary_email": true,
"username_format": "{{identity.entity.metadata.email}}",
}
resp, err := client.Logical().Write("identity/mfa/method/okta", mfaConfigData)
if err != nil || (resp == nil) {
return fmt.Errorf("bad: resp: %#v\n err: %v", resp, err)
}
methodID = resp.Data["method_id"].(string)
if methodID == "" {
return fmt.Errorf("method ID is empty")
}
// creating MFAEnforcementConfig
_, err = client.Logical().Write("identity/mfa/login-enforcement/randomName", map[string]interface{}{
"auth_method_accessors": []string{mountAccessor},
"auth_method_types": []string{"userpass"},
"identity_entity_ids": []string{entityID},
"name": "randomName",
"mfa_method_ids": []string{methodID},
})
if err != nil {
return fmt.Errorf("failed to configure MFAEnforcementConfig: %v", err)
}
}
secret, err = client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{
"password": "testpassword",
})
if err != nil {
return fmt.Errorf("failed to login using userpass auth: %v", err)
}
if secret.Auth == nil || secret.Auth.MFARequirement == nil {
return fmt.Errorf("two phase login returned nil MFARequirement")
}
if secret.Auth.MFARequirement.MFARequestID == "" {
return fmt.Errorf("MFARequirement contains empty MFARequestID")
}
if secret.Auth.MFARequirement.MFAConstraints == nil || len(secret.Auth.MFARequirement.MFAConstraints) == 0 {
return fmt.Errorf("MFAConstraints is nil or empty")
}
mfaConstraints, ok := secret.Auth.MFARequirement.MFAConstraints["randomName"]
if !ok {
return fmt.Errorf("failed to find the mfaConstrains")
}
if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 {
return fmt.Errorf("")
}
for _, mfaAny := range mfaConstraints.Any {
if mfaAny.ID != methodID || mfaAny.Type != "okta" {
return fmt.Errorf("invalid mfa constraints")
}
}
// validation
secret, err = client.Sys().MFAValidateWithContext(context.Background(),
secret.Auth.MFARequirement.MFARequestID,
map[string]interface{}{
methodID: []string{},
},
)
if err != nil {
return fmt.Errorf("MFA failed: %v", err)
}
userpassToken = secret.Auth.ClientToken
if secret.Auth.ClientToken == "" {
return fmt.Errorf("MFA was not enforced")
}
client.SetToken(client.Token())
secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{
"token": userpassToken,
})
if err != nil {
return fmt.Errorf("failed to lookup userpass authenticated token: %v", err)
}
entityIDCheck := secret.Data["entity_id"].(string)
if entityIDCheck != entityID {
return fmt.Errorf("different entityID assigned")
}
return nil
}

View File

@ -347,230 +347,6 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
},
},
},
{
Pattern: "mfa/method/okta" + genericOptionalUUIDRegex("method_id"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "mfa",
},
Fields: map[string]*framework.FieldSchema{
"method_name": {
Type: framework.TypeString,
Description: `The unique name identifier for this MFA method.`,
},
"method_id": {
Type: framework.TypeString,
Description: `The unique identifier for this MFA method.`,
},
"username_format": {
Type: framework.TypeString,
Description: `A template string for mapping Identity names to MFA method names. Values to substitute should be placed in {{}}. For example, "{{entity.name}}@example.com". If blank, the Entity's name field will be used as-is.`,
},
"org_name": {
Type: framework.TypeString,
Description: "Name of the organization to be used in the Okta API.",
},
"api_token": {
Type: framework.TypeString,
Description: "Okta API key.",
},
"base_url": {
Type: framework.TypeString,
Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`,
},
"primary_email": {
Type: framework.TypeBool,
Description: `If true, the username will only match the primary email for the account. Defaults to false.`,
},
"production": {
Type: framework.TypeBool,
Description: "(DEPRECATED) Use base_url instead.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: i.handleMFAMethodOKTARead,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "okta-method-configuration|okta-method-configuration",
},
Summary: "Read the current configuration for the given MFA method",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: i.handleMFAMethodOKTAUpdate,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "configure",
OperationSuffix: "okta-method|okta-method",
},
Summary: "Update or create a configuration for the given MFA method",
},
logical.DeleteOperation: &framework.PathOperation{
Callback: i.handleMFAMethodOKTADelete,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "delete",
OperationSuffix: "okta-method|okta-method",
},
Summary: "Delete a configuration for the given MFA method",
},
},
},
{
Pattern: "mfa/method/okta/?$",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: i.handleMFAMethodListOkta,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "mfa",
OperationVerb: "list",
OperationSuffix: "okta-methods",
},
Summary: "List MFA method configurations for the given MFA method",
},
},
},
{
Pattern: "mfa/method/duo" + genericOptionalUUIDRegex("method_id"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "mfa",
},
Fields: map[string]*framework.FieldSchema{
"method_name": {
Type: framework.TypeString,
Description: `The unique name identifier for this MFA method.`,
},
"method_id": {
Type: framework.TypeString,
Description: `The unique identifier for this MFA method.`,
},
"username_format": {
Type: framework.TypeString,
Description: `A template string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: alias.name: The name returned by the mount configured via the mount_accessor parameter If blank, the Alias's name field will be used as-is. `,
},
"secret_key": {
Type: framework.TypeString,
Description: "Secret key for Duo.",
},
"integration_key": {
Type: framework.TypeString,
Description: "Integration key for Duo.",
},
"api_hostname": {
Type: framework.TypeString,
Description: "API host name for Duo.",
},
"push_info": {
Type: framework.TypeString,
Description: "Push information for Duo.",
},
"use_passcode": {
Type: framework.TypeBool,
Description: `If true, the user is reminded to use the passcode upon MFA validation. This option does not enforce using the passcode. Defaults to false.`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: i.handleMFAMethodDuoRead,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "duo-method-configuration|duo-method-configuration",
},
Summary: "Read the current configuration for the given MFA method",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: i.handleMFAMethodDuoUpdate,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "configure",
OperationSuffix: "duo-method|duo-method",
},
Summary: "Update or create a configuration for the given MFA method",
},
logical.DeleteOperation: &framework.PathOperation{
Callback: i.handleMFAMethodDUODelete,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "delete",
OperationSuffix: "duo-method|duo-method",
},
Summary: "Delete a configuration for the given MFA method",
},
},
},
{
Pattern: "mfa/method/duo/?$",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: i.handleMFAMethodListDuo,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "mfa",
OperationVerb: "list",
OperationSuffix: "duo-methods",
},
Summary: "List MFA method configurations for the given MFA method",
},
},
},
{
Pattern: "mfa/method/pingid" + genericOptionalUUIDRegex("method_id"),
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "mfa",
},
Fields: map[string]*framework.FieldSchema{
"method_name": {
Type: framework.TypeString,
Description: `The unique name identifier for this MFA method.`,
},
"method_id": {
Type: framework.TypeString,
Description: `The unique identifier for this MFA method.`,
},
"username_format": {
Type: framework.TypeString,
Description: `A template string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: alias.name: The name returned by the mount configured via the mount_accessor parameter If blank, the Alias's name field will be used as-is. `,
},
"settings_file_base64": {
Type: framework.TypeString,
Description: "The settings file provided by Ping, Base64-encoded. This must be a settings file suitable for third-party clients, not the PingID SDK or PingFederate.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: i.handleMFAMethodPingIDRead,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "read",
OperationSuffix: "ping-id-method-configuration|ping-id-method-configuration",
},
Summary: "Read the current configuration for the given MFA method",
},
logical.UpdateOperation: &framework.PathOperation{
Callback: i.handleMFAMethodPingIDUpdate,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "configure",
OperationSuffix: "ping-id-method|ping-id-method",
},
Summary: "Update or create a configuration for the given MFA method",
},
logical.DeleteOperation: &framework.PathOperation{
Callback: i.handleMFAMethodPingIDDelete,
DisplayAttrs: &framework.DisplayAttributes{
OperationVerb: "delete",
OperationSuffix: "ping-id-method|ping-id-method",
},
Summary: "Delete a configuration for the given MFA method",
},
},
},
{
Pattern: "mfa/method/pingid/?$",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: i.handleMFAMethodListPingID,
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: "mfa",
OperationVerb: "list",
OperationSuffix: "ping-id-methods",
},
Summary: "List MFA method configurations for the given MFA method",
},
},
},
{
Pattern: "mfa/login-enforcement/" + framework.GenericNameRegex("name"),
DisplayAttrs: &framework.DisplayAttributes{

View File

@ -188,9 +188,6 @@ var (
"mfa/method/totp/" + framework.GenericNameRegex("name") + "/admin-generate$": {parameters: []string{"name"}, operations: []logical.Operation{logical.UpdateOperation}},
"mfa/method/totp/" + framework.GenericNameRegex("name") + "/admin-destroy$": {parameters: []string{"name"}, operations: []logical.Operation{logical.UpdateOperation}},
"mfa/method/totp/" + framework.GenericNameRegex("name"): {parameters: []string{"name"}, operations: []logical.Operation{logical.DeleteOperation, logical.ReadOperation, logical.UpdateOperation}},
"mfa/method/okta/" + framework.GenericNameRegex("name"): {parameters: []string{"name"}, operations: []logical.Operation{logical.DeleteOperation, logical.ReadOperation, logical.UpdateOperation}},
"mfa/method/duo/" + framework.GenericNameRegex("name"): {parameters: []string{"name"}, operations: []logical.Operation{logical.DeleteOperation, logical.ReadOperation, logical.UpdateOperation}},
"mfa/method/pingid/" + framework.GenericNameRegex("name"): {parameters: []string{"name"}, operations: []logical.Operation{logical.DeleteOperation, logical.ReadOperation, logical.UpdateOperation}},
})...)
// control-group paths

View File

@ -7,22 +7,15 @@ import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"image/png"
"io/ioutil"
"net/http"
"net/url"
"strings"
"sync"
"time"
duoapi "github.com/duosecurity/duo_api_golang"
"github.com/duosecurity/duo_api_golang/authapi"
"github.com/golang-jwt/jwt/v4"
"github.com/golang/protobuf/proto"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-multierror"
@ -31,15 +24,11 @@ import (
"github.com/hashicorp/vault/helper/identity/mfa"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/identitytpl"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"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"
@ -47,9 +36,6 @@ import (
const (
mfaMethodTypeTOTP = "totp"
mfaMethodTypeDuo = "duo"
mfaMethodTypeOkta = "okta"
mfaMethodTypePingID = "pingid"
memDBLoginMFAConfigsTable = "login_mfa_configs"
memDBMFALoginEnforcementsTable = "login_enforcements"
mfaTOTPKeysPrefix = systemBarrierPrefix + "mfa/totpkeys/"
@ -185,18 +171,6 @@ func (i *IdentityStore) handleMFAMethodListTOTP(ctx context.Context, req *logica
return i.handleMFAMethodList(ctx, req, d, mfaMethodTypeTOTP)
}
func (i *IdentityStore) handleMFAMethodListDuo(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodList(ctx, req, d, mfaMethodTypeDuo)
}
func (i *IdentityStore) handleMFAMethodListOkta(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodList(ctx, req, d, mfaMethodTypeOkta)
}
func (i *IdentityStore) handleMFAMethodListPingID(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodList(ctx, req, d, mfaMethodTypePingID)
}
func (i *IdentityStore) handleMFAMethodListGlobal(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
keys, configInfo, err := i.mfaBackend.mfaMethodList(ctx, "")
if err != nil {
@ -219,18 +193,6 @@ func (i *IdentityStore) handleMFAMethodTOTPRead(ctx context.Context, req *logica
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypeTOTP)
}
func (i *IdentityStore) handleMFAMethodOKTARead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypeOkta)
}
func (i *IdentityStore) handleMFAMethodDuoRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypeDuo)
}
func (i *IdentityStore) handleMFAMethodPingIDRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodReadCommon(ctx, req, d, mfaMethodTypePingID)
}
func (i *IdentityStore) handleMFAMethodReadGlobal(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodReadCommon(ctx, req, d, "")
}
@ -360,24 +322,6 @@ func (i *IdentityStore) handleMFAMethodUpdateCommon(ctx context.Context, req *lo
return logical.ErrorResponse(err.Error()), nil
}
case mfaMethodTypeOkta:
err = parseOktaConfig(mConfig, d)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
case mfaMethodTypeDuo:
err = parseDuoConfig(mConfig, d)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
case mfaMethodTypePingID:
err = parsePingIDConfig(mConfig, d)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
default:
return logical.ErrorResponse(fmt.Sprintf("unrecognized type %q", methodType)), nil
}
@ -409,34 +353,10 @@ func (i *IdentityStore) handleMFAMethodTOTPUpdate(ctx context.Context, req *logi
return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypeTOTP)
}
func (i *IdentityStore) handleMFAMethodOKTAUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypeOkta)
}
func (i *IdentityStore) handleMFAMethodDuoUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypeDuo)
}
func (i *IdentityStore) handleMFAMethodPingIDUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypePingID)
}
func (i *IdentityStore) handleMFAMethodTOTPDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodDeleteCommon(ctx, req, d, mfaMethodTypeTOTP)
}
func (i *IdentityStore) handleMFAMethodOKTADelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodDeleteCommon(ctx, req, d, mfaMethodTypeOkta)
}
func (i *IdentityStore) handleMFAMethodDUODelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodDeleteCommon(ctx, req, d, mfaMethodTypeDuo)
}
func (i *IdentityStore) handleMFAMethodPingIDDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
return i.handleMFAMethodDeleteCommon(ctx, req, d, mfaMethodTypePingID)
}
func (i *IdentityStore) handleMFAMethodDeleteCommon(ctx context.Context, req *logical.Request, d *framework.FieldData, methodType string) (*logical.Response, error) {
methodID := d.Get("method_id").(string)
if methodID == "" {
@ -1229,91 +1149,6 @@ func (b *MFABackend) handleMFAGenerateTOTP(ctx context.Context, mConfig *mfa.Con
}, nil
}
func parseDuoConfig(mConfig *mfa.Config, d *framework.FieldData) error {
secretKey := d.Get("secret_key").(string)
if secretKey == "" {
return fmt.Errorf("secret_key is empty")
}
integrationKey := d.Get("integration_key").(string)
if integrationKey == "" {
return fmt.Errorf("integration_key is empty")
}
apiHostname := d.Get("api_hostname").(string)
if apiHostname == "" {
return fmt.Errorf("api_hostname is empty")
}
config := &mfa.DuoConfig{
SecretKey: secretKey,
IntegrationKey: integrationKey,
APIHostname: apiHostname,
PushInfo: d.Get("push_info").(string),
UsePasscode: d.Get("use_passcode").(bool),
}
mConfig.Config = &mfa.Config_DuoConfig{
DuoConfig: config,
}
return nil
}
func parsePingIDConfig(mConfig *mfa.Config, d *framework.FieldData) error {
fileString := d.Get("settings_file_base64").(string)
if fileString == "" {
return fmt.Errorf("settings_file_base64 is empty")
}
fileBytes, err := base64.StdEncoding.DecodeString(fileString)
if err != nil {
return err
}
config := &mfa.PingIDConfig{}
for _, line := range strings.Split(string(fileBytes), "\n") {
if strings.HasPrefix(line, "#") {
continue
}
if strings.TrimSpace(line) == "" {
continue
}
splitLine := strings.SplitN(line, "=", 2)
if len(splitLine) != 2 {
return fmt.Errorf("pingid settings file contains a non-empty non-comment line that is not in key=value format: %q", line)
}
switch splitLine[0] {
case "use_base64_key":
config.UseBase64Key = splitLine[1]
case "use_signature":
result, err := parseutil.ParseBool(splitLine[1])
if err != nil {
return errors.New("error parsing use_signature value in pingid settings file")
}
config.UseSignature = result
case "token":
config.Token = splitLine[1]
case "idp_url":
config.IDPURL = splitLine[1]
case "org_alias":
config.OrgAlias = splitLine[1]
case "admin_url":
config.AdminURL = splitLine[1]
case "authenticator_url":
config.AuthenticatorURL = splitLine[1]
default:
return fmt.Errorf("unknown key %q in pingid settings file", splitLine[0])
}
}
mConfig.Config = &mfa.Config_PingIDConfig{
PingIDConfig: config,
}
return nil
}
func (b *LoginMFABackend) mfaConfigReadByMethodID(id string) (map[string]interface{}, error) {
mConfig, err := b.MemDBMFAConfigByID(id)
if err != nil {
@ -1483,30 +1318,6 @@ func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config) (map[string]interface{}
respData["qr_size"] = totpConfig.QRSize
respData["algorithm"] = otplib.Algorithm(totpConfig.Algorithm).String()
respData["max_validation_attempts"] = totpConfig.MaxValidationAttempts
case *mfa.Config_OktaConfig:
oktaConfig := mConfig.GetOktaConfig()
respData["org_name"] = oktaConfig.OrgName
if oktaConfig.BaseURL != "" {
respData["base_url"] = oktaConfig.BaseURL
} else {
respData["production"] = oktaConfig.Production
}
respData["mount_accessor"] = mConfig.MountAccessor
respData["username_format"] = mConfig.UsernameFormat
case *mfa.Config_DuoConfig:
duoConfig := mConfig.GetDuoConfig()
respData["api_hostname"] = duoConfig.APIHostname
respData["pushinfo"] = duoConfig.PushInfo
respData["mount_accessor"] = mConfig.MountAccessor
respData["username_format"] = mConfig.UsernameFormat
respData["use_passcode"] = duoConfig.UsePasscode
case *mfa.Config_PingIDConfig:
pingConfig := mConfig.GetPingIDConfig()
respData["use_signature"] = pingConfig.UseSignature
respData["idp_url"] = pingConfig.IDPURL
respData["org_alias"] = pingConfig.OrgAlias
respData["admin_url"] = pingConfig.AdminURL
respData["authenticator_url"] = pingConfig.AuthenticatorURL
default:
return nil, fmt.Errorf("invalid method type %q was persisted, underlying type: %T", mConfig.Type, mConfig.Config)
}
@ -1607,63 +1418,6 @@ func parseTOTPConfig(mConfig *mfa.Config, d *framework.FieldData) error {
return nil
}
func parseOktaConfig(mConfig *mfa.Config, d *framework.FieldData) error {
if mConfig == nil {
return errors.New("config is nil")
}
if d == nil {
return errors.New("field data is nil")
}
oktaConfig := &mfa.OktaConfig{}
orgName := d.Get("org_name").(string)
if orgName == "" {
return errors.New("org_name must be set")
}
oktaConfig.OrgName = orgName
token := d.Get("api_token").(string)
if token == "" {
return errors.New("api_token must be set")
}
oktaConfig.APIToken = token
productionRaw, productionOk := d.GetOk("production")
if productionOk {
oktaConfig.Production = productionRaw.(bool)
} else {
oktaConfig.Production = true
}
baseURLRaw, ok := d.GetOk("base_url")
if ok {
oktaConfig.BaseURL = baseURLRaw.(string)
} else {
// Only set if not using legacy production flag
if !productionOk {
oktaConfig.BaseURL = "okta.com"
}
}
primaryEmailOnly := d.Get("primary_email").(bool)
if primaryEmailOnly {
oktaConfig.PrimaryEmail = true
}
_, err := url.Parse(fmt.Sprintf("https://%s,%s", oktaConfig.OrgName, oktaConfig.BaseURL))
if err != nil {
return errwrap.Wrapf("error parsing given base_url: {{err}}", err)
}
mConfig.Config = &mfa.Config_OktaConfig{
OktaConfig: oktaConfig,
}
return nil
}
func (c *Core) validateLoginMFA(ctx context.Context, eConfig *mfa.MFAEnforcementConfig, entity *identity.Entity, requestConnRemoteAddr string, mfaCredsMap logical.MFACreds) error {
sanitizedMfaCreds, err := c.loginMFABackend.sanitizeMFACredsWithLoginEnforcementMethodIDs(ctx, mfaCredsMap, eConfig.MFAMethodIDs)
if err != nil {
@ -1708,31 +1462,6 @@ func (c *Core) validateLoginMFAInternal(ctx context.Context, methodID string, en
return fmt.Errorf("MFA method configuration not present")
}
var finalUsername string
switch mConfig.Type {
case mfaMethodTypeDuo, mfaMethodTypeOkta, mfaMethodTypePingID:
if mConfig.UsernameFormat == "" {
finalUsername = entity.Name
} else {
directGroups, inheritedGroups, err := c.identityStore.groupsByEntityID(entity.ID)
if err != nil {
return fmt.Errorf("failed to fetch group memberships: %w", err)
}
groups := append(directGroups, inheritedGroups...)
_, finalUsername, err = identitytpl.PopulateString(identitytpl.PopulateStringInput{
Mode: identitytpl.ACLTemplating,
String: mConfig.UsernameFormat,
Entity: identity.ToSDKEntity(entity),
Groups: identity.ToSDKGroups(groups),
NamespaceID: entity.NamespaceID,
})
if err != nil {
return err
}
}
}
mfaFactors, err := parseMfaFactors(mfaCreds)
if err != nil {
return fmt.Errorf("failed to parse MFA factor, %w", err)
@ -1751,15 +1480,6 @@ func (c *Core) validateLoginMFAInternal(ctx context.Context, methodID string, en
return c.validateTOTP(ctx, mfaFactors, entityMFASecret, mConfig.ID, entity.ID, c.loginMFABackend.usedCodes, mConfig.GetTOTPConfig().MaxValidationAttempts)
case mfaMethodTypeOkta:
return c.validateOkta(ctx, mConfig, finalUsername)
case mfaMethodTypeDuo:
return c.validateDuo(ctx, mfaFactors, mConfig, finalUsername, reqConnectionRemoteAddress)
case mfaMethodTypePingID:
return c.validatePingID(ctx, mConfig, finalUsername)
default:
return fmt.Errorf("unrecognized MFA type %q", mConfig.Type)
}
@ -1906,468 +1626,6 @@ func parseMfaFactors(creds []string) (*MFAFactor, error) {
return mfaFactor, nil
}
func (c *Core) validateDuo(ctx context.Context, mfaFactors *MFAFactor, mConfig *mfa.Config, username, reqConnectionRemoteAddr string) error {
duoConfig := mConfig.GetDuoConfig()
if duoConfig == nil {
return fmt.Errorf("failed to get Duo configuration for method %q", mConfig.Name)
}
var passcode string
if mfaFactors != nil {
passcode = mfaFactors.passcode
}
client := duoapi.NewDuoApi(
duoConfig.IntegrationKey,
duoConfig.SecretKey,
duoConfig.APIHostname,
duoConfig.PushInfo,
)
authClient := authapi.NewAuthApi(*client)
check, err := authClient.Check()
if err != nil {
return err
}
if check == nil {
return errors.New("Duo api check returned nil, possibly bad integration key")
}
var message string
var messageDetail string
if check.StatResult.Message != nil {
message = *check.StatResult.Message
}
if check.StatResult.Message_Detail != nil {
messageDetail = *check.StatResult.Message_Detail
}
if check.StatResult.Stat != "OK" {
return fmt.Errorf("check against Duo failed; message (if given): %q; message detail (if given): %q", message, messageDetail)
}
preauth, err := authClient.Preauth(authapi.PreauthUsername(username), authapi.PreauthIpAddr(reqConnectionRemoteAddr))
if err != nil {
return errwrap.Wrapf("failed to perform Duo preauth: {{err}}", err)
}
if preauth == nil {
return fmt.Errorf("failed to perform Duo preauth")
}
if preauth.StatResult.Stat != "OK" {
return fmt.Errorf("failed to perform Duo preauth: %q - %q", *preauth.StatResult.Message, *preauth.StatResult.Message_Detail)
}
switch preauth.Response.Result {
case "allow":
return nil
case "deny":
return fmt.Errorf(preauth.Response.Status_Msg)
case "enroll":
return fmt.Errorf(fmt.Sprintf("%q - %q", preauth.Response.Status_Msg, preauth.Response.Enroll_Portal_Url))
case "auth":
break
default:
return fmt.Errorf("invalid response from Duo preauth: %q", preauth.Response.Result)
}
options := []func(*url.Values){}
factor := "push"
if passcode != "" {
factor = "passcode"
options = append(options, authapi.AuthPasscode(passcode))
} else {
options = append(options, authapi.AuthDevice("auto"))
if duoConfig.PushInfo != "" {
options = append(options, authapi.AuthPushinfo(duoConfig.PushInfo))
}
}
options = append(options, authapi.AuthIpAddr(reqConnectionRemoteAddr))
options = append(options, authapi.AuthUsername(username))
options = append(options, authapi.AuthAsync())
result, err := authClient.Auth(factor, options...)
if err != nil {
return errwrap.Wrapf("failed to authenticate with Duo: {{err}}", err)
}
if result.StatResult.Stat != "OK" {
return fmt.Errorf("failed to authenticate with Duo: %q - %q", *result.StatResult.Message, *result.StatResult.Message_Detail)
}
if result.Response.Txid == "" {
return fmt.Errorf("failed to get transaction ID for Duo authentication")
}
for {
// AuthStatus does the long polling until there is a status update. So
// there is no need to wait for a second before we invoke this API.
statusResult, err := authClient.AuthStatus(result.Response.Txid)
if err != nil {
return errwrap.Wrapf("failed to get authentication status from Duo: {{err}}", err)
}
if statusResult == nil {
return errwrap.Wrapf("failed to get authentication status from Duo: {{err}}", err)
}
if statusResult.StatResult.Stat != "OK" {
return fmt.Errorf("failed to get authentication status from Duo: %q - %q", *statusResult.StatResult.Message, *statusResult.StatResult.Message_Detail)
}
switch statusResult.Response.Result {
case "deny":
return fmt.Errorf("duo authentication failed: %q", statusResult.Response.Status_Msg)
case "allow":
return nil
}
timer := time.NewTimer(time.Second)
select {
case <-ctx.Done():
timer.Stop()
return fmt.Errorf("duo push verification operation canceled")
case <-timer.C:
}
}
}
func (c *Core) validateOkta(ctx context.Context, mConfig *mfa.Config, username string) error {
oktaConfig := mConfig.GetOktaConfig()
if oktaConfig == nil {
return fmt.Errorf("failed to get Okta configuration for method %q", mConfig.Name)
}
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
}
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 {
filterField = "profile.email"
}
filterQuery := fmt.Sprintf("%s eq %q", filterField, username)
filter := query.NewQueryParams(query.WithFilter(filterQuery))
users, _, err := client.User.ListUsers(ctx, filter)
if err != nil {
return err
}
switch {
case len(users) == 0:
return fmt.Errorf("no users found for e-mail address")
case len(users) > 1:
return fmt.Errorf("more than one user found for e-mail address")
}
user := users[0]
factors, _, err := client.UserFactor.ListFactors(ctx, user.Id)
if err != nil {
return err
}
if len(factors) == 0 {
return fmt.Errorf("no MFA factors found for user")
}
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 !factorFound {
return fmt.Errorf("no push-type MFA factor found for user")
}
result, _, err := client.UserFactor.VerifyFactor(ctx, user.Id, userFactor.Id, okta.VerifyFactorRequest{}, userFactor, nil)
if err != nil {
return err
}
if result.FactorResult != "WAITING" {
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 {
// 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 *okta.VerifyUserFactorResponse
_, err = rq.Do(ctx, req, &result)
if err != nil {
return err
}
switch result.FactorResult {
case "WAITING":
case "SUCCESS":
return nil
case "REJECTED":
return fmt.Errorf("push verification explicitly rejected")
case "TIMEOUT":
return fmt.Errorf("push verification timed out")
default:
return fmt.Errorf("unknown status code")
}
timer := time.NewTimer(time.Second)
select {
case <-ctx.Done():
timer.Stop()
return fmt.Errorf("push verification operation canceled")
case <-timer.C:
}
}
}
func (c *Core) validatePingID(ctx context.Context, mConfig *mfa.Config, username string) error {
pingConfig := mConfig.GetPingIDConfig()
if pingConfig == nil {
return fmt.Errorf("failed to get PingID configuration for method %q", mConfig.Name)
}
signingKey, err := base64.StdEncoding.DecodeString(pingConfig.UseBase64Key)
if err != nil {
return errwrap.Wrapf("failed decoding pingid signing key: {{err}}", err)
}
client := cleanhttp.DefaultClient()
createRequest := func(reqPath string, reqBody map[string]interface{}) (*http.Request, error) {
// Construct the token
token := &jwt.Token{
Method: jwt.SigningMethodHS256,
Header: map[string]interface{}{
"alg": "HS256",
"org_alias": pingConfig.OrgAlias,
"token": pingConfig.Token,
},
Claims: jwt.MapClaims{
"reqHeader": map[string]interface{}{
"locale": "en",
"orgAlias": pingConfig.OrgAlias,
"secretKey": pingConfig.Token,
"timestamp": time.Now().Format("2006-01-02 15:04:05.000"),
"version": "4.9",
},
"reqBody": reqBody,
},
}
signedToken, err := token.SignedString(signingKey)
if err != nil {
return nil, errwrap.Wrapf("failed signing pingid request token: {{err}}", err)
}
// Construct the URL
if !strings.HasPrefix(reqPath, "/") {
reqPath = "/" + reqPath
}
reqURL, err := url.Parse(pingConfig.IDPURL + reqPath)
if err != nil {
return nil, errwrap.Wrapf("failed to parse pingid request url: {{err}}", err)
}
// Construct the request; WithContext is done here since it's a shallow
// copy
req := &http.Request{}
req = req.WithContext(ctx)
req.Method = "POST"
req.URL = reqURL
req.Body = ioutil.NopCloser(bytes.NewBufferString(signedToken))
if req.Header == nil {
req.Header = make(http.Header)
}
req.Header.Set("Content-Type", "application/json")
return req, nil
}
do := func(req *http.Request) (*jwt.Token, error) {
// Run the request and fetch the response
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("nil response from pingid")
}
if resp.Body == nil {
return nil, fmt.Errorf("nil body in pingid response")
}
bodyBytes := bytes.NewBuffer(nil)
_, err = bodyBytes.ReadFrom(resp.Body)
resp.Body.Close()
if err != nil {
return nil, errwrap.Wrapf("error reading pingid response: {{err}}", err)
}
// Parse the body, which is a JWT. Ensure that it's using HMAC signing
// and return the signing key in the func for validation
token, err := jwt.Parse(bodyBytes.String(), func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method %q from pingid response", token.Header["alg"])
}
return signingKey, nil
})
if err != nil {
return nil, errwrap.Wrapf("error parsing pingid response: {{err}}", err)
}
// Check if parameters are as expected
if _, ok := token.Header["token"]; !ok {
return nil, fmt.Errorf("%q header not found", "token")
}
if headerTokenStr, ok := token.Header["token"].(string); !ok || headerTokenStr != pingConfig.Token {
return nil, fmt.Errorf("invalid token in ping response")
}
// validate org alias
// This was originally 'org_alias', but it appears to now be returned as 'orgAlias'. Official
// Ping docs are sparse on the header details. We now prefer orgAlias but will still handle
// org_alias.
oa := token.Header["orgAlias"]
if oa == nil {
if oa = token.Header["org_alias"]; oa == nil {
return nil, fmt.Errorf("neither orgAlias nor org_alias headers were found")
}
}
if headerOrgAliasStr, ok := oa.(string); !ok || headerOrgAliasStr != pingConfig.OrgAlias {
return nil, fmt.Errorf("invalid org_alias in ping response")
}
return token, nil
}
type deviceDetails struct {
PushEnabled bool `mapstructure:"pushEnabled"`
DeviceID int64 `mapstructure:"deviceId"`
}
type respBody struct {
SessionID string `mapstructure:"sessionId"`
ErrorID int64 `mapstructure:"errorId"`
ErrorMsg string `mapstructure:"errorMsg"`
UserDevices []deviceDetails `mapstructure:"userDevices"`
}
type apiResponse struct {
ResponseBody respBody `mapstructure:"responseBody"`
}
/*
// Normally we don't leave in commented code, however:
// Explicitly setting the device ID didn't work even when the device was
// push enabled (said the application was not installed on the device), so
// instead trigger default behavior, which does work, even when there's
// only one device and the deviceid matched :-/
// We're leaving this here because if we support other types we'll likely
// still need it, and if we get device ID selection working we'll want it.
req, err := createRequest("rest/4/startauthentication/do", map[string]interface{}{
"spAlias": "web",
"userName": username,
})
if err != nil {
return err
}
token, err := do(req)
if err != nil {
return err
}
// We get back a map from the JWT library so use mapstructure
var startResp apiResponse
err = mapstructure.Decode(token.Claims, &startResp)
if err != nil {
return err
}
// Look for at least one push-enabled method
body := startResp.ResponseBody
var foundPush bool
switch {
case body.ErrorID != 30007:
return fmt.Errorf("only pingid push authentication is currently supported")
case len(body.UserDevices) == 0:
return fmt.Errorf("no user mfa devices returned from pingid")
default:
for _, dev := range body.UserDevices {
if dev.PushEnabled {
foundPush = true
break
}
}
if !foundPush {
return fmt.Errorf("no push enabled device id found from pingid")
}
}
*/
req, err := createRequest("rest/4/authonline/do", map[string]interface{}{
"spAlias": "web",
"userName": username,
"authType": "CONFIRM",
})
if err != nil {
return err
}
token, err := do(req)
if err != nil {
return err
}
// Ensure a success response
var authResp apiResponse
err = mapstructure.Decode(token.Claims, &authResp)
if err != nil {
return err
}
if authResp.ResponseBody.ErrorID != 200 {
return errors.New(authResp.ResponseBody.ErrorMsg)
}
return nil
}
func (c *Core) validateTOTP(ctx context.Context, mfaFactors *MFAFactor, entityMethodSecret *mfa.Secret, configID, entityID string, usedCodes *cache.Cache, maximumValidationAttempts uint32) error {
if mfaFactors == nil || mfaFactors.passcode == "" {
return fmt.Errorf("MFA credentials not supplied")
@ -3016,16 +2274,4 @@ var mfaHelp = map[string][2]string{
"Defines or updates a TOTP MFA method.",
"",
},
"okta-method": {
"Defines or updates an Okta MFA method.",
"",
},
"duo-method": {
"Defines or updates a Duo MFA method.",
"",
},
"pingid-method": {
"Defines or updates a PingID MFA method.",
"",
},
}

View File

@ -2170,18 +2170,10 @@ func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*
if err != nil {
return nil, fmt.Errorf("failed to get methodID %s from MFA config table, error: %v", methodID, err)
}
var duoUsePasscode bool
if mConfig.Type == mfaMethodTypeDuo {
duoConf, ok := mConfig.Config.(*mfa.Config_DuoConfig)
if !ok {
return nil, fmt.Errorf("invalid MFA configuration type")
}
duoUsePasscode = duoConf.DuoConfig.UsePasscode
}
mfaMethod := &logical.MFAMethodID{
Type: mConfig.Type,
ID: methodID,
UsesPasscode: mConfig.Type == mfaMethodTypeTOTP || duoUsePasscode,
UsesPasscode: mConfig.Type == mfaMethodTypeTOTP,
Name: mConfig.Name,
}
mfaAny.Any = append(mfaAny.Any, mfaMethod)