Okta implementation (#1966)

This commit is contained in:
Shane Starcher
2017-01-26 19:08:52 -05:00
committed by Jeff Mitchell
parent 73ad5b7da8
commit a0b5eecc6d
18 changed files with 1464 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
package okta
import (
"fmt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
}
func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Help: backendHelp,
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"login/*",
},
},
Paths: append([]*framework.Path{
pathConfig(&b),
pathUsers(&b),
pathGroups(&b),
pathUsersList(&b),
pathGroupsList(&b),
pathLogin(&b),
}),
AuthRenew: b.pathLoginRenew,
}
return &b
}
type backend struct {
*framework.Backend
}
func (b *backend) Login(req *logical.Request, username string, password string) ([]string, *logical.Response, error) {
cfg, err := b.Config(req.Storage)
if err != nil {
return nil, nil, err
}
if cfg == nil {
return nil, logical.ErrorResponse("Okta backend not configured"), nil
}
client := cfg.OktaClient()
auth, err := client.Authenticate(username, password)
if err != nil {
return nil, logical.ErrorResponse(fmt.Sprintf("Okta auth failed: %v", err)), nil
}
if auth == nil {
return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil
}
if b.Logger().IsDebug() {
b.Logger().Debug("auth/okta:", auth)
}
oktaGroups, err := b.getOktaGroups(cfg, auth.Embedded.User.ID)
if err != nil {
return nil, logical.ErrorResponse(err.Error()), nil
}
if b.Logger().IsDebug() {
b.Logger().Debug("auth/okta: Groups fetched from Okta", "num_groups", len(oktaGroups), "groups", oktaGroups)
}
oktaResponse := &logical.Response{
Data: map[string]interface{}{},
}
if len(oktaGroups) == 0 {
errString := fmt.Sprintf(
"no Okta groups found; only policies from locally-defined groups available")
oktaResponse.AddWarning(errString)
}
var allGroups []string
// Import the custom added groups from okta backend
user, err := b.User(req.Storage, username)
if err == nil && user != nil && user.Groups != nil {
if b.Logger().IsDebug() {
b.Logger().Debug("auth/okta: adding local groups", "num_local_groups", len(user.Groups), "local_groups", user.Groups)
}
allGroups = append(allGroups, user.Groups...)
}
// Merge local and Okta groups
allGroups = append(allGroups, oktaGroups...)
// Retrieve policies
var policies []string
for _, groupName := range allGroups {
group, err := b.Group(req.Storage, groupName)
if err == nil && group != nil {
policies = append(policies, group.Policies...)
}
}
// Merge local Policies into Okta Policies
policies = append(policies, user.Policies...)
if len(policies) == 0 {
errStr := "user is not a member of any authorized policy"
if len(oktaResponse.Warnings()) > 0 {
errStr = fmt.Sprintf("%s; additionally, %s", errStr, oktaResponse.Warnings()[0])
}
oktaResponse.Data["error"] = errStr
return nil, oktaResponse, nil
}
return policies, oktaResponse, nil
}
func (b *backend) getOktaGroups(cfg *ConfigEntry, userID string) ([]string, error) {
if cfg.Token != "" {
client := cfg.OktaClient()
groups, err := client.Groups(userID)
if err != nil {
return nil, err
}
oktaGroups := make([]string, 0, len(*groups))
for _, group := range *groups {
oktaGroups = append(oktaGroups, group.Profile.Name)
}
return oktaGroups, err
}
return nil, nil
}
const backendHelp = `
The Okta credential provider allows authentication querying,
checking username and password, and associating policies. If an api token is configure
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 suppying the two fields for "login".
`

View File

@@ -0,0 +1,173 @@
package okta
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/vault/helper/logformat"
log "github.com/mgutz/logxi/v1"
"github.com/hashicorp/vault/logical"
logicaltest "github.com/hashicorp/vault/logical/testing"
)
func TestBackend_Config(t *testing.T) {
b, err := Factory(&logical.BackendConfig{
Logger: logformat.NewVaultLogger(log.LevelTrace),
System: &logical.StaticSystemView{},
})
if err != nil {
t.Fatalf("Unable to create backend: %s", err)
}
username := os.Getenv("OKTA_USERNAME")
password := os.Getenv("OKTA_PASSWORD")
configData := map[string]interface{}{
"organization": os.Getenv("OKTA_ORG"),
"base_url": "oktapreview.com",
}
configDataToken := map[string]interface{}{
"token": os.Getenv("OKTA_API_TOKEN"),
}
logicaltest.Test(t, logicaltest.TestCase{
AcceptanceTest: true,
PreCheck: func() { testAccPreCheck(t) },
Backend: b,
Steps: []logicaltest.TestStep{
testConfigCreate(t, configData),
testLoginWrite(t, username, "wrong", "E0000004", 0),
testLoginWrite(t, username, password, "user is not a member of any authorized policy", 0),
testAccUserGroups(t, username, "local_group,local_group2"),
testAccGroups(t, "local_group", "local_group_policy"),
testLoginWrite(t, username, password, "", 2),
testAccGroups(t, "Everyone", "everyone_group_policy,every_group_policy2"),
testLoginWrite(t, username, password, "", 2),
testConfigUpdate(t, configDataToken),
testConfigRead(t, configData),
testLoginWrite(t, username, password, "", 4),
testAccGroups(t, "TestGroup", "testgroup_group_policy"),
testLoginWrite(t, username, password, "", 5),
},
})
}
func testLoginWrite(t *testing.T, username, password, reason string, policies int) 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()
}
}
if resp.Auth != nil {
if len(resp.Auth.Policies) != policies {
return fmt.Errorf("policy mismatch expected %d but got %s", policies, resp.Auth.Policies)
}
}
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, 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"] != d["organization"] {
return fmt.Errorf("Org mismatch expected %s but got %s", d["organization"], resp.Data["Org"])
}
if resp.Data["BaseURL"] != d["base_url"] {
return fmt.Errorf("BaseURL mismatch expected %s but got %s", d["base_url"], resp.Data["BaseURL"])
}
if _, exists := resp.Data["Token"]; exists {
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")
}
}
func testAccUserGroups(t *testing.T, user string, groups string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "users/" + user,
Data: map[string]interface{}{
"groups": groups,
},
}
}
func testAccGroups(t *testing.T, group string, policies string) logicaltest.TestStep {
t.Logf("[testAccGroups] - Registering group %s, policy %s", group, policies)
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "groups/" + group,
Data: map[string]interface{}{
"policies": policies,
},
}
}
func testAccLogin(t *testing.T, user, password string, keys []string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login/" + user,
Data: map[string]interface{}{
"password": password,
},
Unauthenticated: true,
Check: logicaltest.TestCheckAuth(keys),
}
}

View File

@@ -0,0 +1,66 @@
package okta
import (
"fmt"
"os"
"strings"
"github.com/hashicorp/vault/api"
pwd "github.com/hashicorp/vault/helper/password"
)
// CLIHandler struct
type CLIHandler struct{}
// Auth cli method
func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (string, error) {
mount, ok := m["mount"]
if !ok {
mount = "okta"
}
username, ok := m["username"]
if !ok {
return "", fmt.Errorf("'username' var must be set")
}
password, ok := m["password"]
if !ok {
fmt.Printf("Password (will be hidden): ")
var err error
password, err = pwd.Read(os.Stdin)
fmt.Println()
if err != nil {
return "", err
}
}
data := map[string]interface{}{
"password": password,
}
path := fmt.Sprintf("auth/%s/login/%s", mount, username)
secret, err := c.Logical().Write(path, data)
if err != nil {
return "", err
}
if secret == nil {
return "", fmt.Errorf("empty response from credential provider")
}
return secret.Auth.ClientToken, nil
}
// Help method for okta cli
func (h *CLIHandler) Help() string {
help := `
The Okta credential provider allows you to authenticate with Okta.
To use it, first configure it through the "config" endpoint, and then
login by specifying username and password. If password is not provided
on the command line, it will be read from stdin.
Example: vault auth -method=okta username=john
`
return strings.TrimSpace(help)
}

View File

@@ -0,0 +1,169 @@
package okta
import (
"fmt"
"net/url"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
"github.com/sstarcher/go-okta"
)
func pathConfig(b *backend) *framework.Path {
return &framework.Path{
Pattern: `config`,
Fields: map[string]*framework.FieldSchema{
"organization": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Okta organization to authenticate against",
},
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Okta admin API token",
},
"base_url": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The API endpoint to use. Useful if you
are using Okta development accounts.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathConfigRead,
logical.CreateOperation: b.pathConfigWrite,
logical.UpdateOperation: b.pathConfigWrite,
},
ExistenceCheck: b.pathConfigExistenceCheck,
HelpSynopsis: pathConfigHelp,
}
}
// Config returns the configuration for this backend.
func (b *backend) Config(s logical.Storage) (*ConfigEntry, error) {
entry, err := s.Get("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
}
}
return &result, nil
}
func (b *backend) pathConfigRead(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg, err := b.Config(req.Storage)
if err != nil {
return nil, err
}
if cfg == nil {
return nil, nil
}
resp := &logical.Response{
Data: map[string]interface{}{
"Org": cfg.Org,
"BaseURL": cfg.BaseURL,
},
}
return resp, nil
}
func (b *backend) pathConfigWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
org := d.Get("organization").(string)
cfg, err := b.Config(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: org,
}
}
token, ok := d.GetOk("token")
if ok {
cfg.Token = token.(string)
} else if req.Operation == logical.CreateOperation {
cfg.Token = d.Get("token").(string)
}
baseURL, ok := d.GetOk("base_url")
if ok {
baseURLString := baseURL.(string)
if len(baseURLString) != 0 {
_, err = url.Parse(baseURLString)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error parsing given base_url: %s", err)), nil
}
cfg.BaseURL = baseURLString
}
} else if req.Operation == logical.CreateOperation {
cfg.BaseURL = d.Get("base_url").(string)
}
jsonCfg, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
return nil, err
}
if err := req.Storage.Put(jsonCfg); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathConfigExistenceCheck(
req *logical.Request, d *framework.FieldData) (bool, error) {
cfg, err := b.Config(req.Storage)
if err != nil {
return false, err
}
return cfg != nil, nil
}
// OktaClient creates a basic okta client connection
func (c *ConfigEntry) OktaClient() *okta.Client {
client := okta.NewClient(c.Org)
if c.BaseURL != "" {
client.Url = c.BaseURL
}
if c.Token != "" {
client.ApiToken = c.Token
}
return client
}
// ConfigEntry for Okta
type ConfigEntry struct {
Org string `json:"organization"`
Token string `json:"token"`
BaseURL string `json:"base_url"`
}
const pathConfigHelp = `
This endpoint allows you to configure the Okta and its
configuration options.
The Okta organization are the characters at the front of the URL for Okta.
Example https://ORG.okta.com
`

View File

@@ -0,0 +1,147 @@
package okta
import (
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathGroupsList(b *backend) *framework.Path {
return &framework.Path{
Pattern: "groups/?$",
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>.+)`,
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the Okta group.",
},
"policies": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Comma-separated 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,
}
}
func (b *backend) Group(s logical.Storage, n string) (*GroupEntry, error) {
entry, err := s.Get("group/" + n)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result GroupEntry
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathGroupDelete(
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("group/" + name)
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathGroupRead(
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
}
group, err := b.Group(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(
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
}
entry, err := logical.StorageEntryJSON("group/"+name, &GroupEntry{
Policies: policyutil.ParsePolicies(d.Get("policies").(string)),
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathGroupList(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
groups, err := req.Storage.List("group/")
if err != nil {
return nil, err
}
return logical.ListResponse(groups), nil
}
type GroupEntry struct {
Policies []string
}
const pathGroupHelpSyn = `
Manage users allowed to authenticate.
`
const pathGroupHelpDesc = `
This endpoint allows you to create, read, update, and delete configuration
for Okta groups that are allowed to authenticate, and associate policies to
them.
Deleting a group will not revoke auth for prior authenticated users in that
group. To do this, do a revoke on "login/<username>" for
the usernames you want revoked.
`

View File

@@ -0,0 +1,99 @@
package okta
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: `login/(?P<username>.+)`,
Fields: map[string]*framework.FieldSchema{
"username": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Username to be used for login.",
},
"password": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Password for this user.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathLogin,
},
HelpSynopsis: pathLoginSyn,
HelpDescription: pathLoginDesc,
}
}
func (b *backend) pathLogin(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := d.Get("username").(string)
password := d.Get("password").(string)
policies, resp, err := b.Login(req, username, password)
// 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{}
}
sort.Strings(policies)
resp.Auth = &logical.Auth{
Policies: policies,
Metadata: map[string]string{
"username": username,
"policies": strings.Join(policies, ","),
},
InternalData: map[string]interface{}{
"password": password,
},
DisplayName: username,
LeaseOptions: logical.LeaseOptions{
Renewable: true,
},
}
return resp, nil
}
func (b *backend) pathLoginRenew(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
username := req.Auth.Metadata["username"]
password := req.Auth.InternalData["password"].(string)
loginPolicies, resp, err := b.Login(req, username, password)
if len(loginPolicies) == 0 {
return resp, err
}
if !policyutil.EquivalentPolicies(loginPolicies, req.Auth.Policies) {
return nil, fmt.Errorf("policies have changed, not renewing")
}
return framework.LeaseExtend(0, 0, b.System())(req, d)
}
const pathLoginSyn = `
Log in with a username and password.
`
const pathLoginDesc = `
This endpoint authenticates using a username and password.
`

View File

@@ -0,0 +1,166 @@
package okta
import (
"strings"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathUsersList(b *backend) *framework.Path {
return &framework.Path{
Pattern: "users/?$",
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>.+)`,
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the user.",
},
"groups": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Comma-separated list of groups associated with the user.",
},
"policies": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Comma-separated 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(s logical.Storage, n string) (*UserEntry, error) {
entry, err := s.Get("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(
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("user/" + name)
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathUserRead(
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(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(
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 := strings.Split(d.Get("groups").(string), ",")
for i, g := range groups {
groups[i] = strings.TrimSpace(g)
}
policies := strings.Split(d.Get("policies").(string), ",")
for i, p := range policies {
policies[i] = strings.TrimSpace(p)
}
// Store it
entry, err := logical.StorageEntryJSON("user/"+name, &UserEntry{
Groups: groups,
Policies: policies,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathUserList(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
users, err := req.Storage.List("user/")
if err != nil {
return nil, err
}
return logical.ListResponse(users), nil
}
type UserEntry struct {
Groups []string
Policies []string
}
const pathUserHelpSyn = `
Manage additional groups for users allowed to authenticate.
`
const pathUserHelpDesc = `
This endpoint allows you to create, read, update, and delete configuration
for Okta users that are allowed to authenticate, in particular associating
additional groups to them.
Deleting a user will not revoke their auth. To do this, do a revoke on "login/<username>" for
the usernames you want revoked.
`

View File

@@ -13,6 +13,7 @@ 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"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/builtin/logical/aws"
@@ -72,6 +73,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
"github": credGitHub.Factory,
"userpass": credUserpass.Factory,
"ldap": credLdap.Factory,
"okta": credOkta.Factory,
},
LogicalBackends: map[string]logical.Factory{
"aws": aws.Factory,
@@ -110,6 +112,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
"github": &credGitHub.CLIHandler{},
"userpass": &credUserpass.CLIHandler{},
"ldap": &credLdap.CLIHandler{},
"okta": &credOkta.CLIHandler{},
"cert": &credCert.CLIHandler{},
},
}, nil

9
vendor/github.com/sstarcher/go-okta/README.md generated vendored Normal file
View File

@@ -0,0 +1,9 @@
Okta golang client
================
[![CircleCI](https://circleci.com/gh/sstarcher/job-reaper.svg?style=svg)](https://circleci.com/gh/sstarcher/go-okta)
Basic Okta HTTP client

124
vendor/github.com/sstarcher/go-okta/api.go generated vendored Normal file
View File

@@ -0,0 +1,124 @@
package okta
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
)
// Client to access okta
type Client struct {
client *http.Client
org string
Url string
ApiToken string
}
// errorResponse is an error wrapper for the okta response
type errorResponse struct {
HTTPCode int
Response ErrorResponse
Endpoint string
}
func (e *errorResponse) Error() string {
return fmt.Sprintf("Error hitting api endpoint %s %s", e.Endpoint, e.Response.ErrorCode)
}
// NewClient object for calling okta
func NewClient(org string) *Client {
client := Client{
client: &http.Client{},
org: org,
Url: "okta.com",
}
return &client
}
// Authenticate with okta using username and password
func (c *Client) Authenticate(username, password string) (*AuthnResponse, error) {
var request = &AuthnRequest{
Username: username,
Password: password,
}
var response = &AuthnResponse{}
err := c.call("authn", "POST", request, response)
return response, err
}
// Session takes a session token and always fails
func (c *Client) Session(sessionToken string) (*SessionResponse, error) {
var request = &SessionRequest{
SessionToken: sessionToken,
}
var response = &SessionResponse{}
err := c.call("sessions", "POST", request, response)
return response, err
}
// User takes a user id and returns data about that user
func (c *Client) User(userID string) (*User, error) {
var response = &User{}
err := c.call("users/"+userID, "GET", nil, response)
return response, err
}
// Groups takes a user id and returns the groups the user belongs to
func (c *Client) Groups(userID string) (*Groups, error) {
var response = &Groups{}
err := c.call("users/"+userID+"/groups", "GET", nil, response)
return response, err
}
func (c *Client) call(endpoint, method string, request, response interface{}) error {
data, _ := json.Marshal(request)
var url = "https://" + c.org + "." + c.Url + "/api/v1/" + endpoint
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil {
log.Fatal(err)
}
req.Header.Add("Accept", `application/json`)
req.Header.Add("Content-Type", `application/json`)
if c.ApiToken != "" {
req.Header.Add("Authorization", "SSWS "+c.ApiToken)
}
resp, err := c.client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode == http.StatusOK {
err := json.Unmarshal(body, &response)
if err != nil {
log.Fatal(err)
}
} else {
var errors ErrorResponse
err = json.Unmarshal(body, &errors)
return &errorResponse{
HTTPCode: resp.StatusCode,
Response: errors,
Endpoint: url,
}
}
return nil
}

45
vendor/github.com/sstarcher/go-okta/authn.go generated vendored Normal file
View File

@@ -0,0 +1,45 @@
package okta
import (
"time"
)
type ErrorResponse struct {
ErrorCode string `json:"errorCode"`
ErrorSummary string `json:"errorSummary"`
ErrorLink string `json:"errorLink"`
ErrorID string `json:"errorId"`
ErrorCauses []struct {
ErrorSummary string `json:"errorSummary"`
} `json:"errorCauses"`
}
type AuthnRequest struct {
Username string `json:"username"`
Password string `json:"password"`
RelayState string `json:"relayState"`
Options struct {
MultiOptionalFactorEnroll bool `json:"multiOptionalFactorEnroll"`
WarnBeforePasswordExpired bool `json:"warnBeforePasswordExpired"`
} `json:"options"`
}
type AuthnResponse struct {
ExpiresAt time.Time `json:"expiresAt"`
Status string `json:"status"`
RelayState string `json:"relayState"`
SessionToken string `json:"sessionToken"`
Embedded struct {
User struct {
ID string `json:"id"`
PasswordChanged time.Time `json:"passwordChanged"`
Profile struct {
Login string `json:"login"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Locale string `json:"locale"`
TimeZone string `json:"timeZone"`
} `json:"profile"`
} `json:"user"`
} `json:"_embedded"`
}

19
vendor/github.com/sstarcher/go-okta/circleci.yml generated vendored Normal file
View File

@@ -0,0 +1,19 @@
machine:
environment:
GOPATH: "${HOME}/.go_workspace"
IMPORT_PATH: "${GOPATH}/src/github.com/${CIRCLE_PROJECT_USERNAME}"
APP_PATH: "${IMPORT_PATH}/${CIRCLE_PROJECT_REPONAME}"
dependencies:
override:
- sudo add-apt-repository ppa:masterminds/glide -y
- sudo apt-get update
- sudo apt-get install glide -y
test:
pre:
- mkdir -p "$IMPORT_PATH"
- ln -sf "$(pwd)" "${APP_PATH}"
- cd "${APP_PATH}" && glide install
override:
- cd "${APP_PATH}" && go test -cover $(glide nv)

4
vendor/github.com/sstarcher/go-okta/glide.lock generated vendored Normal file
View File

@@ -0,0 +1,4 @@
hash: acc035e4a3e5e3ed975f4233cc66fdbf3af5eb7bc2b5b337a26f730abf86e4b7
updated: 2016-09-28T11:14:46.44318819-04:00
imports: []
testImports: []

2
vendor/github.com/sstarcher/go-okta/glide.yaml generated vendored Normal file
View File

@@ -0,0 +1,2 @@
package: github.com/sstarcher/go-okta
import: []

46
vendor/github.com/sstarcher/go-okta/sessions.go generated vendored Normal file
View File

@@ -0,0 +1,46 @@
package okta
import (
"time"
)
type SessionRequest struct {
SessionToken string `json:"sessionToken"`
}
type SessionResponse struct {
ID string `json:"id"`
Login string `json:"login"`
UserID string `json:"userId"`
ExpiresAt time.Time `json:"expiresAt"`
Status string `json:"status"`
LastPasswordVerification time.Time `json:"lastPasswordVerification"`
LastFactorVerification interface{} `json:"lastFactorVerification"`
Amr []string `json:"amr"`
Idp struct {
ID string `json:"id"`
Type string `json:"type"`
} `json:"idp"`
MfaActive bool `json:"mfaActive"`
Links struct {
Self struct {
Href string `json:"href"`
Hints struct {
Allow []string `json:"allow"`
} `json:"hints"`
} `json:"self"`
Refresh struct {
Href string `json:"href"`
Hints struct {
Allow []string `json:"allow"`
} `json:"hints"`
} `json:"refresh"`
User struct {
Name string `json:"name"`
Href string `json:"href"`
Hints struct {
Allow []string `json:"allow"`
} `json:"hints"`
} `json:"user"`
} `json:"_links"`
}

83
vendor/github.com/sstarcher/go-okta/users.go generated vendored Normal file
View File

@@ -0,0 +1,83 @@
package okta
import (
"time"
)
type User struct {
ID string `json:"id"`
Status string `json:"status"`
Created *time.Time `json:"created"`
Activated *time.Time `json:"activated"`
StatusChanged *time.Time `json:"statusChanged"`
LastLogin *time.Time `json:"lastLogin"`
LastUpdated *time.Time `json:"lastUpdated"`
PasswordChanged *time.Time `json:"passwordChanged"`
Profile struct {
Login string `json:"login"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
NickName string `json:"nickName"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
SecondEmail string `json:"secondEmail"`
ProfileURL string `json:"profileUrl"`
PreferredLanguage string `json:"preferredLanguage"`
UserType string `json:"userType"`
Organization string `json:"organization"`
Title string `json:"title"`
Division string `json:"division"`
Department string `json:"department"`
CostCenter string `json:"costCenter"`
EmployeeNumber string `json:"employeeNumber"`
MobilePhone string `json:"mobilePhone"`
PrimaryPhone string `json:"primaryPhone"`
StreetAddress string `json:"streetAddress"`
City string `json:"city"`
State string `json:"state"`
ZipCode string `json:"zipCode"`
CountryCode string `json:"countryCode"`
} `json:"profile"`
Credentials struct {
Password struct {
} `json:"password"`
RecoveryQuestion struct {
Question string `json:"question"`
} `json:"recovery_question"`
Provider struct {
Type string `json:"type"`
Name string `json:"name"`
} `json:"provider"`
} `json:"credentials"`
Links struct {
ResetPassword struct {
Href string `json:"href"`
} `json:"resetPassword"`
ResetFactors struct {
Href string `json:"href"`
} `json:"resetFactors"`
ExpirePassword struct {
Href string `json:"href"`
} `json:"expirePassword"`
ForgotPassword struct {
Href string `json:"href"`
} `json:"forgotPassword"`
ChangeRecoveryQuestion struct {
Href string `json:"href"`
} `json:"changeRecoveryQuestion"`
Deactivate struct {
Href string `json:"href"`
} `json:"deactivate"`
ChangePassword struct {
Href string `json:"href"`
} `json:"changePassword"`
} `json:"_links"`
}
type Groups []struct {
ID string `json:"id"`
Profile struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"profile"`
}

5
vendor/vendor.json vendored
View File

@@ -967,6 +967,11 @@
"revisionTime": "2016-12-29T17:44:48Z"
},
{
"checksumSHA1": "reJ+wO9qzH/7r2vXQE5MiTvg8+w=",
"path": "github.com/sstarcher/go-okta",
"revision": "388b6aef4eed400621bd3e3a98d831ef1368582d",
"revisionTime": "2016-10-03T17:19:47Z"
},
"checksumSHA1": "MWqyOvDMkW+XYe2RJ5mplvut+aE=",
"path": "github.com/ugorji/go/codec",
"revision": "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74",

View File

@@ -0,0 +1,159 @@
---
layout: "docs"
page_title: "Auth Backend: Okta"
sidebar_current: "docs-auth-okta"
description: |-
The Okta auth backend allows users to authenticate with Vault using Okta credentials.
---
# Auth Backend: Okta
Name: `okta`
The Okta auth backend allows authentication using Okta
and user/password credentials. This allows Vault to be integrated
into environments using Okta.
The mapping of groups in Okta to Vault policies is managed by using the
`users/` and `groups/` paths.
## Authentication
#### Via the CLI
```
$ vault auth -method=okta username=mitchellh
Password (will be hidden):
Successfully authenticated! The policies that are associated
with this token are listed below:
admins
```
#### Via the API
The endpoint for the login is `auth/okta/login/<username>`.
The password should be sent in the POST body encoded as JSON.
```shell
$ curl $VAULT_ADDR/v1/auth/okta/login/mitchellh \
-d '{ "password": "foo" }'
```
The response will be in JSON. For example:
```javascript
{
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"auth": {
"client_token": "c4f280f6-fdb2-18eb-89d3-589e2e834cdb",
"policies": [
"admins"
],
"metadata": {
"username": "mitchellh"
},
"lease_duration": 0,
"renewable": false
}
}
```
## Configuration
First, you must enable the Okta auth backend:
```
$ vault auth-enable okta
Successfully enabled 'okta' at 'okta'!
```
Now when you run `vault auth -methods`, the Okta backend is available:
```
Path Type Description
okta/ okta
token/ token token based credentials
```
To use the Okta auth backend, it must first be configured for your Okta account.
The configuration options are categorized and detailed below.
Configuration is written to `auth/okta/config`.
### Connection parameters
* `organization` (string, required) - The Okta organization. This will be the first part of the url `https://XXX.okta.com` url.
* `token` (string, optional) - The 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. This can be generated from http://developer.okta.com/docs/api/getting_started/getting_a_token.html
* `base_url` (string, optional) - The Okta url. Examples: `oktapreview.com`, The default is `okta.com`
Use `vault path-help` for more details.
## Examples:
### Scenario 1
* Okta organization `XXXTest`.
* With no token supplied only locally configured group membership will be available. Groups will not be queried from Okta.
```
$ vault write auth/okta/config \
organization="XXXTest"
...
```
### Scenario 2
* Okta organization `dev-123456`.
* Okta base_url for developer account `oktapreview.com`
* API token `00KzlTNCqDf0enpQKYSAYUt88KHqXax6dT11xEZz_g`. This will allow group membership to be queried.
```
$ vault write auth/okta/config base_url="oktapreview.com" \
organization="dev-123456" \
token="00KzlTNCqDf0enpQKYSAYUt88KHqXax6dT11xEZz_g"
...
```
## Okta Group -> Policy Mapping
Next we want to create a mapping from an Okta group to a Vault policy:
```
$ vault write auth/okta/groups/scientists policies=foo,bar
```
This maps the Okta group "scientists" to the "foo" and "bar" Vault policies.
We can also add specific Okta users to additional (potentially non-Okta) groups:
```
$ vault write auth/okta/groups/engineers policies=foobar
$ vault write auth/okta/users/tesla groups=engineers
```
This adds the Okta user "tesla" to the "engineers" group, which maps to
the "foobar" Vault policy.
Finally, we can test this by authenticating:
```
$ vault auth -method=okta username=tesla
Password (will be hidden):
Successfully authenticated! The policies that are associated
with this token are listed below:
bar, foo, foobar
```
## Note on Okta Group's
Groups can only be pulled from Okta if an API token is configured via `token`
## Note on policy mapping
It should be noted that user -> policy mapping (via group membership) happens at token creation time. And changes in group membership in Okta will not affect tokens that have already been provisioned. To see these changes, old tokens should be revoked and the user should be asked to reauthenticate.