Adding support for base_url for Okta api (#3316)

* 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
This commit is contained in:
Chris Hoffman 2017-09-15 00:27:45 -04:00 committed by GitHub
parent 4a8c33cca3
commit 3aa68c0034
9 changed files with 137 additions and 88 deletions

View File

@ -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{}{},
}
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...)
}
var allGroups []string
// Import the custom added groups from okta backend
user, err := b.User(req.Storage, username)
if err != nil {
@ -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
@ -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

View File

@ -3,7 +3,6 @@ package okta
import (
"fmt"
"net/url"
"strings"
"time"
@ -13,13 +12,18 @@ 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,
@ -27,7 +31,7 @@ func pathConfig(b *backend) *framework.Path {
},
"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,
@ -35,13 +39,11 @@ func pathConfig(b *backend) *framework.Path {
},
"base_url": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The API endpoint to use. Useful if you
are using Okta development accounts. (DEPRECATED)`,
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,
@ -100,7 +102,6 @@ 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,
},
@ -108,6 +109,9 @@ func (b *backend) pathConfigRead(
if cfg.BaseURL != "" {
resp.Data["base_url"] = cfg.BaseURL
}
if cfg.Production != nil {
resp.Data["production"] = *cfg.Production
}
return resp, nil
}
@ -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)
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 = baseURLString
}
} 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 {
@ -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

View File

@ -21,8 +21,9 @@ import (
const (
libraryVersion = "1"
userAgent = "oktasdk-go/" + libraryVersion
productionURLFormat = "https://%s.okta.com/api/v1/"
previewProductionURLFormat = "https://%s.oktapreview.com/api/v1/"
productionDomain = "okta.com"
previewDomain = "oktapreview.com"
urlFormat = "https://%s.%s/api/v1/"
headerRateLimit = "X-Rate-Limit-Limit"
headerRateRemaining = "X-Rate-Limit-Remaining"
headerRateReset = "X-Rate-Limit-Reset"
@ -94,19 +95,38 @@ type service struct {
// NewClient returns a new OKTA API client. If a nil httpClient is
// provided, http.DefaultClient will be used.
func NewClient(httpClient *http.Client, orgName string, apiToken string, isProduction bool) *Client {
var baseDomain string
if isProduction {
baseDomain = productionDomain
} else {
baseDomain = previewDomain
}
client, _ := NewClientWithDomain(httpClient, orgName, baseDomain, apiToken)
return client
}
// NewClientWithDomain creates a client based on the organziation name and
// base domain for requests (okta.com, okta-emea.com, oktapreview.com, etc).
func NewClientWithDomain(httpClient *http.Client, orgName string, domain string, apiToken string) (*Client, error) {
baseURL, err := url.Parse(fmt.Sprintf(urlFormat, orgName, domain))
if err != nil {
return nil, err
}
return NewClientWithBaseURL(httpClient, baseURL, apiToken), nil
}
// NewClientWithBaseURL creates a client based on the full base URL and api
// token
func NewClientWithBaseURL(httpClient *http.Client, baseURL *url.URL, apiToken string) *Client {
if httpClient == nil {
httpClient = http.DefaultClient
}
var baseURL *url.URL
if isProduction {
baseURL, _ = url.Parse(fmt.Sprintf(productionURLFormat, orgName))
} else {
baseURL, _ = url.Parse(fmt.Sprintf(previewProductionURLFormat, orgName))
c := &Client{
client: httpClient,
BaseURL: baseURL,
UserAgent: userAgent,
}
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
c.PauseOnRateLimit = true // If rate limit found it will block until that time. If false then Error will be returned
c.authorizationHeaderValue = fmt.Sprintf(headerAuthorizationFormat, apiToken)
c.apiKey = apiToken

6
vendor/vendor.json vendored
View File

@ -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=",

View File

@ -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.
@ -83,7 +84,7 @@ $ curl \
"data": {
"org_name": "example",
"api_token": "abc123",
"production": true,
"base_url": "okta.com",
"ttl": "",
"max_ttl": ""
},

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -87,8 +87,8 @@ 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
* `org_name` (string, required) - The Okta organization. This will be the first part of the url `https://XXX.okta.com` url.
* `api_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`
* `max_ttl` (string, optional) - Maximum duration after which authentication will be expired.
Either number of seconds or in a format parsable by Go's [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)
@ -106,7 +106,7 @@ Use `vault path-help` for more details.
```
$ vault write auth/okta/config \
organization="XXXTest"
org_name="XXXTest"
...
```
@ -118,8 +118,8 @@ $ vault write auth/okta/config \
```
$ vault write auth/okta/config base_url="oktapreview.com" \
organization="dev-123456" \
token="00KzlTNCqDf0enpQKYSAYUt88KHqXax6dT11xEZz_g"
org_name="dev-123456" \
api_token="00KzlTNCqDf0enpQKYSAYUt88KHqXax6dT11xEZz_g"
...
```