Skip to content

Commit

Permalink
Adding support for base_url for Okta api (hashicorp#3316)
Browse files Browse the repository at this point in the history
* Adding support for base_url for Okta api

* addressing feedback suggestions, bringing back optional group query

* updating docs

* cleaning up the login method

* clear out production flag if base_url is set

* docs updates

* docs updates
  • Loading branch information
chrishoffman authored Sep 15, 2017
1 parent 4a8c33c commit 3aa68c0
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 87 deletions.
55 changes: 32 additions & 23 deletions builtin/credential/okta/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,25 @@ func (b *backend) Login(req *logical.Request, username string, password string)
return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil
}

oktaUser := &result.Embedded.User
rsp, err = client.Users.PopulateGroups(oktaUser)
if err != nil {
return nil, logical.ErrorResponse(err.Error()), nil
}
if rsp == nil {
return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil
}
oktaGroups := make([]string, 0, len(oktaUser.Groups))
for _, group := range oktaUser.Groups {
oktaGroups = append(oktaGroups, group.Profile.Name)
}
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
// Only query the Okta API for group membership if we have a token
if cfg.Token != "" {
oktaGroups, err := b.getOktaGroups(client, &result.Embedded.User)
if err != nil {
return nil, logical.ErrorResponse(fmt.Sprintf("okta failure retrieving groups: %v", err)), 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(req.Storage, username)
if err != nil {
Expand All @@ -122,8 +115,6 @@ func (b *backend) Login(req *logical.Request, username string, password string)
}
allGroups = append(allGroups, user.Groups...)
}
// Merge local and Okta groups
allGroups = append(allGroups, oktaGroups...)

// Retrieve policies
var policies []string
Expand Down Expand Up @@ -157,6 +148,24 @@ func (b *backend) Login(req *logical.Request, username string, password string)
return policies, oktaResponse, nil
}

func (b *backend) getOktaGroups(client *okta.Client, user *okta.User) ([]string, error) {
rsp, err := client.Users.PopulateGroups(user)
if err != nil {
return nil, err
}
if rsp == nil {
return nil, fmt.Errorf("okta auth backend unexpected failure")
}
oktaGroups := make([]string, 0, len(user.Groups))
for _, group := range user.Groups {
oktaGroups = append(oktaGroups, group.Profile.Name)
}
if b.Logger().IsDebug() {
b.Logger().Debug("auth/okta: Groups fetched from Okta", "num_groups", len(oktaGroups), "groups", 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 configure
Expand Down
70 changes: 40 additions & 30 deletions builtin/credential/okta/path_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package okta
import (
"fmt"
"net/url"
"strings"

"time"

Expand All @@ -13,35 +12,38 @@ import (
"github.com/hashicorp/vault/logical/framework"
)

const (
defaultBaseURL = "okta.com"
previewBaseURL = "oktapreview.com"
)

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 (DEPRECATED)",
Description: "(DEPRECATED) Okta organization to authenticate against. Use org_name instead.",
},
"org_name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the organization to be used in the Okta API.",
},
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Okta admin API token (DEPRECATED)",
Description: "(DEPRECATED) Okta admin API token. Use api_token instead.",
},
"api_token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Okta API key.",
},
"base_url": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The API endpoint to use. Useful if you
are using Okta development accounts. (DEPRECATED)`,
Type: framework.TypeString,
Description: `The base domain to use for the Okta API. When not specified in the configuraiton, "okta.com" is used.`,
},
"production": &framework.FieldSchema{
Type: framework.TypeBool,
Default: true,
Description: `If set, production API URL prefix will be used to communicate with Okta and if not set, a preview production API URL prefix will be used. Defaults to true.`,
Description: `(DEPRECATED) Use base_url.`,
},
"ttl": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Expand Down Expand Up @@ -100,14 +102,16 @@ func (b *backend) pathConfigRead(
Data: map[string]interface{}{
"organization": cfg.Org,
"org_name": cfg.Org,
"production": *cfg.Production,
"ttl": cfg.TTL,
"max_ttl": cfg.MaxTTL,
},
}
if cfg.BaseURL != "" {
resp.Data["base_url"] = cfg.BaseURL
}
if cfg.Production != nil {
resp.Data["production"] = *cfg.Production
}

return resp, nil
}
Expand Down Expand Up @@ -149,26 +153,29 @@ func (b *backend) pathConfigWrite(
cfg.Token = token.(string)
}
}
if cfg.Token == "" && req.Operation == logical.CreateOperation {
return logical.ErrorResponse("api_token is missing"), nil
}

baseURL, ok := d.GetOk("base_url")
baseURLRaw, 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
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
}
} else if req.Operation == logical.CreateOperation {
cfg.BaseURL = d.Get("base_url").(string)
cfg.BaseURL = baseURL
}

productionRaw := d.Get("production").(bool)
cfg.Production = &productionRaw
// 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
}

ttl, ok := d.GetOk("ttl")
if ok {
Expand Down Expand Up @@ -207,16 +214,19 @@ func (b *backend) pathConfigExistenceCheck(

// OktaClient creates a basic okta client connection
func (c *ConfigEntry) OktaClient() *okta.Client {
production := true
baseURL := defaultBaseURL
if c.Production != nil {
production = *c.Production
if !*c.Production {
baseURL = previewBaseURL
}
}
if c.BaseURL != "" {
if strings.Contains(c.BaseURL, "oktapreview.com") {
production = false
}
baseURL = c.BaseURL
}
return okta.NewClient(cleanhttp.DefaultClient(), c.Org, c.Token, production)

// We validate config on input and errors are only returned when parsing URLs
client, _ := okta.NewClientWithDomain(cleanhttp.DefaultClient(), c.Org, baseURL, c.Token)
return client
}

// ConfigEntry for Okta
Expand Down
60 changes: 40 additions & 20 deletions vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions vendor/vendor.json
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,10 @@
"revisionTime": "2017-07-11T19:02:43Z"
},
{
"checksumSHA1": "QZtBo/fc3zeQFxPFgPVMyDiw70M=",
"checksumSHA1": "sFjc2R+KS9AeXIPMV4KCw+GwX5I=",
"path": "github.com/chrismalek/oktasdk-go/okta",
"revision": "7d4ce0a254ec5f9eda3397523f6cf183e1d46c5e",
"revisionTime": "2017-02-07T05:01:14Z"
"revision": "ae553c909ca06a4c34eb41ee435e83871a7c2496",
"revisionTime": "2017-09-11T15:31:29Z"
},
{
"checksumSHA1": "WsB6y1Yd+kDbHGz1Rm7xZ44hyAE=",
Expand Down
11 changes: 6 additions & 5 deletions website/source/api/auth/okta/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ distinction between the `create` and `update` capabilities inside ACL policies.

- `org_name` `(string: <required>)` - Name of the organization to be used in the
Okta API.
- `api_token` `(string: <required>)` - Okta API key.
- `production` `(bool: true)` - If set, production API URL prefix will be used
to communicate with Okta and if not set, a preview production API URL prefix
will be used. Defaults to true.
- `api_token` `(string: "")` - 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.
- `base_url` `(string: "")` - If set, will be used as the base domain
for API requests. Examples are okta.com, oktapreview.com, and okta-emea.com.
- `ttl` `(string: "")` - Duration after which authentication will be expired.
- `max_ttl` `(string: "")` - Maximum duration after which authentication will
be expired.
Expand Down Expand Up @@ -83,7 +84,7 @@ $ curl \
"data": {
"org_name": "example",
"api_token": "abc123",
"production": true,
"base_url": "okta.com",
"ttl": "",
"max_ttl": ""
},
Expand Down
3 changes: 3 additions & 0 deletions website/source/api/system/mfa-duo.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ This endpoint defines a MFA method of type Duo.

- `username_format` `(string)` - A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`. For example, `"{{persona.name}}@example.com"`. If blank, the Persona's Name field will be used as-is. Currently-supported mappings:
- persona.name: The name returned by the mount configured via the `mount_accessor` parameter
- entity.name: The name configured for the Entity
- persona.metadata.`<key>`: The value of the Persona's metadata parameter
- entity.metadata.`<key>`: The value of the Entity's metadata paramater

- `secret_key` `(string)` - Secret key for Duo.

Expand Down
5 changes: 4 additions & 1 deletion website/source/api/system/mfa-okta.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ This endpoint defines a MFA method of type Okta.

- `username_format` `(string)` - A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`. For example, `"{{persona.name}}@example.com"`. If blank, the Persona's Name field will be used as-is. Currently-supported mappings:
- persona.name: The name returned by the mount configured via the `mount_accessor` parameter
- entity.name: The name configured for the Entity
- persona.metadata.`<key>`: The value of the Persona's metadata parameter
- entity.metadata.`<key>`: The value of the Entity's metadata paramater

- `org_name` `(string)` - Name of the organization to be used in the Okta API.

- `api_token` `(string)` - Okta API key.

- `production` `(string)` - If set, production API URL prefix will be used to communicate with Okta and if not set, a preview production API URL prefix will be used. Defaults to true.
- `base_url` `(string)` - If set, will be used as the base domain for API requests. Examples are okta.com, oktapreview.com, and okta-emea.com.

### Sample Payload

Expand Down
3 changes: 3 additions & 0 deletions website/source/api/system/mfa-pingid.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ This endpoint defines a MFA method of type PingID.

- `username_format` `(string)` - A format string for mapping Identity names to MFA method names. Values to substitute should be placed in `{{}}`. For example, `"{{persona.name}}@example.com"`. If blank, the Persona's Name field will be used as-is. Currently-supported mappings:
- persona.name: The name returned by the mount configured via the `mount_accessor` parameter
- entity.name: The name configured for the Entity
- persona.metadata.`<key>`: The value of the Persona's metadata parameter
- entity.metadata.`<key>`: The value of the Entity's metadata paramater

- `settings_file_base64` `(string)` - A base64-encoded third-party settings file retrieved from PingID's configuration page.

Expand Down
Loading

0 comments on commit 3aa68c0

Please sign in to comment.