remove external mfa (pingid, duo, okta)
This commit is contained in:
parent
2acf0a21fd
commit
de439ac574
|
@ -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".
|
||||
`
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 don’t support plugin AutoMTLS
|
||||
TLSProviderFunc: tlsProviderFunc,
|
||||
}); err != nil {
|
||||
logger := hclog.New(&hclog.LoggerOptions{})
|
||||
|
||||
logger.Error("plugin shutting down", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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.
|
||||
`
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
`
|
|
@ -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.
|
||||
`
|
|
@ -107,7 +107,6 @@ func (b *BaseCommand) PredictVaultAvailableAuths() complete.Predictor {
|
|||
"cert",
|
||||
"github",
|
||||
"ldap",
|
||||
"okta",
|
||||
"plugin",
|
||||
"radius",
|
||||
"userpass",
|
||||
|
|
|
@ -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
7
go.mod
|
@ -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
8
go.sum
|
@ -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=
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import AuthConfig from './_base';
|
||||
|
||||
export default AuthConfig.extend();
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -90,7 +90,6 @@ export default Controller.extend({
|
|||
},
|
||||
cancelAuthentication() {
|
||||
this.set('cancelAuth', true);
|
||||
this.set('waitingForOktaNumberChallenge', false);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}),
|
||||
});
|
|
@ -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',
|
||||
|
|
|
@ -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}`];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 = '';
|
||||
}
|
||||
},
|
||||
});
|
|
@ -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 = '';
|
||||
}
|
||||
},
|
||||
});
|
|
@ -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',
|
||||
});
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.",
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user