open-vault/builtin/credential/okta/backend.go

345 lines
10 KiB
Go
Raw Normal View History

2017-01-27 00:08:52 +00:00
package okta
import (
"context"
2017-01-27 00:08:52 +00:00
"fmt"
"time"
2017-01-27 00:08:52 +00:00
"github.com/hashicorp/vault/helper/mfa"
"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"
2017-01-27 00:08:52 +00:00
)
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
Backend plugin system (#2874) * Add backend plugin changes * Fix totp backend plugin tests * Fix logical/plugin InvalidateKey test * Fix plugin catalog CRUD test, fix NoopBackend * Clean up commented code block * Fix system backend mount test * Set plugin_name to omitempty, fix handleMountTable config parsing * Clean up comments, keep shim connections alive until cleanup * Include pluginClient, disallow LookupPlugin call from within a plugin * Add wrapper around backendPluginClient for proper cleanup * Add logger shim tests * Add logger, storage, and system shim tests * Use pointer receivers for system view shim * Use plugin name if no path is provided on mount * Enable plugins for auth backends * Add backend type attribute, move builtin/plugin/package * Fix merge conflict * Fix missing plugin name in mount config * Add integration tests on enabling auth backend plugins * Remove dependency cycle on mock-plugin * Add passthrough backend plugin, use logical.BackendType to determine lease generation * Remove vault package dependency on passthrough package * Add basic impl test for passthrough plugin * Incorporate feedback; set b.backend after shims creation on backendPluginServer * Fix totp plugin test * Add plugin backends docs * Fix tests * Fix builtin/plugin tests * Remove flatten from PluginRunner fields * Move mock plugin to logical/plugin, remove totp and passthrough plugins * Move pluginMap into newPluginClient * Do not create storage RPC connection on HandleRequest and HandleExistenceCheck * Change shim logger's Fatal to no-op * Change BackendType to uint32, match UX backend types * Change framework.Backend Setup signature * Add Setup func to logical.Backend interface * Move OptionallyEnableMlock call into plugin.Serve, update docs and comments * Remove commented var in plugin package * RegisterLicense on logical.Backend interface (#3017) * Add RegisterLicense to logical.Backend interface * Update RegisterLicense to use callback func on framework.Backend * Refactor framework.Backend.RegisterLicense * plugin: Prevent plugin.SystemViewClient.ResponseWrapData from getting JWTs * plugin: Revert BackendType to remove TypePassthrough and related references * Fix typo in plugin backends docs
2017-07-20 17:28:40 +00:00
b := Backend()
if err := b.Setup(ctx, conf); err != nil {
Backend plugin system (#2874) * Add backend plugin changes * Fix totp backend plugin tests * Fix logical/plugin InvalidateKey test * Fix plugin catalog CRUD test, fix NoopBackend * Clean up commented code block * Fix system backend mount test * Set plugin_name to omitempty, fix handleMountTable config parsing * Clean up comments, keep shim connections alive until cleanup * Include pluginClient, disallow LookupPlugin call from within a plugin * Add wrapper around backendPluginClient for proper cleanup * Add logger shim tests * Add logger, storage, and system shim tests * Use pointer receivers for system view shim * Use plugin name if no path is provided on mount * Enable plugins for auth backends * Add backend type attribute, move builtin/plugin/package * Fix merge conflict * Fix missing plugin name in mount config * Add integration tests on enabling auth backend plugins * Remove dependency cycle on mock-plugin * Add passthrough backend plugin, use logical.BackendType to determine lease generation * Remove vault package dependency on passthrough package * Add basic impl test for passthrough plugin * Incorporate feedback; set b.backend after shims creation on backendPluginServer * Fix totp plugin test * Add plugin backends docs * Fix tests * Fix builtin/plugin tests * Remove flatten from PluginRunner fields * Move mock plugin to logical/plugin, remove totp and passthrough plugins * Move pluginMap into newPluginClient * Do not create storage RPC connection on HandleRequest and HandleExistenceCheck * Change shim logger's Fatal to no-op * Change BackendType to uint32, match UX backend types * Change framework.Backend Setup signature * Add Setup func to logical.Backend interface * Move OptionallyEnableMlock call into plugin.Serve, update docs and comments * Remove commented var in plugin package * RegisterLicense on logical.Backend interface (#3017) * Add RegisterLicense to logical.Backend interface * Update RegisterLicense to use callback func on framework.Backend * Refactor framework.Backend.RegisterLicense * plugin: Prevent plugin.SystemViewClient.ResponseWrapData from getting JWTs * plugin: Revert BackendType to remove TypePassthrough and related references * Fix typo in plugin backends docs
2017-07-20 17:28:40 +00:00
return nil, err
}
return b, nil
2017-01-27 00:08:52 +00:00
}
func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Help: backendHelp,
PathsSpecial: &logical.Paths{
Root: mfa.MFARootPaths(),
2017-01-27 00:08:52 +00:00
Unauthenticated: []string{
"login/*",
},
SealWrapStorage: []string{
"config",
},
2017-01-27 00:08:52 +00:00
},
Paths: append([]*framework.Path{
pathConfig(&b),
pathUsers(&b),
pathGroups(&b),
pathUsersList(&b),
pathGroupsList(&b),
},
mfa.MFAPaths(b.Backend, pathLogin(&b))...,
),
2017-01-27 00:08:52 +00:00
AuthRenew: b.pathLoginRenew,
BackendType: logical.TypeCredential,
2017-01-27 00:08:52 +00:00
}
return &b
}
type backend struct {
*framework.Backend
}
func (b *backend) Login(ctx context.Context, req *logical.Request, username string, password string) ([]string, *logical.Response, []string, error) {
cfg, err := b.Config(ctx, req.Storage)
2017-01-27 00:08:52 +00:00
if err != nil {
return nil, nil, nil, err
2017-01-27 00:08:52 +00:00
}
if cfg == nil {
return nil, logical.ErrorResponse("Okta auth method not configured"), nil, nil
2017-01-27 00:08:52 +00:00
}
// 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"`
}
type embeddedResult struct {
User okta.User `json:"user"`
Factors []mfaFactor `json:"factors"`
}
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)
2017-01-27 00:08:52 +00:00
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
2017-01-27 00:08:52 +00:00
}
if rsp == nil {
return nil, logical.ErrorResponse("okta auth method unexpected failure"), nil, nil
2017-01-27 00:08:52 +00:00
}
oktaResponse := &logical.Response{
Data: map[string]interface{}{},
}
2018-03-20 18:54:10 +00:00
// More about Okta's Auth transaction state here:
// https://developer.okta.com/docs/api/resources/authn#transaction-state
2018-01-26 18:46:11 +00:00
// 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)
2018-01-26 18:46:11 +00:00
}
return nil, logical.ErrorResponse("okta authentication failed"), nil, nil
case "PASSWORD_EXPIRED":
if b.Logger().IsDebug() {
b.Logger().Debug("password is expired", "user", username)
2018-01-26 18:46:11 +00:00
}
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.
if cfg.BypassOktaMFA {
result.Status = "SUCCESS"
break
}
factorAvailable := false
var selectedFactor mfaFactor
// only okta push is currently supported
for _, v := range result.Embedded.Factors {
if v.Type == "push" && v.Provider == "OKTA" {
factorAvailable = true
selectedFactor = v
}
}
if !factorAvailable {
return nil, logical.ErrorResponse("Okta Verify Push 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,
}
verifyReq, err := shim.NewRequest("POST", requestPath, payload)
if err != nil {
return nil, nil, nil, err
}
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)
2019-04-16 17:05:50 +00:00
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)
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
}
select {
case <-time.After(500 * time.Millisecond):
// Continue
case <-ctx.Done():
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
}
}
2018-01-26 18:46:11 +00:00
case "SUCCESS":
// Do nothing here
default:
if b.Logger().IsDebug() {
b.Logger().Debug("unhandled result status", "status", result.Status)
2018-01-26 18:46:11 +00:00
}
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:
2018-01-26 18:46:11 +00:00
if b.Logger().IsDebug() {
b.Logger().Debug("authentication returned a non-success status", "status", result.Status)
2018-01-26 18:46:11 +00:00
}
return nil, logical.ErrorResponse("okta authentication failed"), nil, nil
}
2017-01-27 00:08:52 +00:00
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...)
}
2017-01-27 00:08:52 +00:00
// 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)
}
}
2017-01-27 00:08:52 +00:00
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)
2017-01-27 00:08:52 +00:00
}
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...)
2017-01-27 00:08:52 +00:00
}
}
// Merge local Policies into Okta Policies
if user != nil && user.Policies != nil {
policies = append(policies, user.Policies...)
}
2017-01-27 00:08:52 +00:00
return policies, oktaResponse, allGroups, nil
2017-01-27 00:08:52 +00:00
}
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
}
2017-01-27 00:08:52 +00:00
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.
2017-01-27 00:08:52 +00:00
Configuration of the connection is done through the "config" and "policies"
endpoints by a user with root access. Authentication is then done
2018-03-20 18:54:10 +00:00
by supplying the two fields for "login".
2017-01-27 00:08:52 +00:00
`