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 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{ oktaResponse := &logical.Response{
Data: map[string]interface{}{}, 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 { if len(oktaGroups) == 0 {
errString := fmt.Sprintf( errString := fmt.Sprintf(
"no Okta groups found; only policies from locally-defined groups available") "no Okta groups found; only policies from locally-defined groups available")
oktaResponse.AddWarning(errString) oktaResponse.AddWarning(errString)
} }
allGroups = append(allGroups, oktaGroups...)
}
var allGroups []string
// Import the custom added groups from okta backend // Import the custom added groups from okta backend
user, err := b.User(req.Storage, username) user, err := b.User(req.Storage, username)
if err != nil { if err != nil {
@ -122,8 +115,6 @@ func (b *backend) Login(req *logical.Request, username string, password string)
} }
allGroups = append(allGroups, user.Groups...) allGroups = append(allGroups, user.Groups...)
} }
// Merge local and Okta groups
allGroups = append(allGroups, oktaGroups...)
// Retrieve policies // Retrieve policies
var policies []string var policies []string
@ -157,6 +148,24 @@ func (b *backend) Login(req *logical.Request, username string, password string)
return policies, oktaResponse, nil 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 = ` const backendHelp = `
The Okta credential provider allows authentication querying, The Okta credential provider allows authentication querying,
checking username and password, and associating policies. If an api token is configure checking username and password, and associating policies. If an api token is configure

View File

@ -3,7 +3,6 @@ package okta
import ( import (
"fmt" "fmt"
"net/url" "net/url"
"strings"
"time" "time"
@ -13,13 +12,18 @@ import (
"github.com/hashicorp/vault/logical/framework" "github.com/hashicorp/vault/logical/framework"
) )
const (
defaultBaseURL = "okta.com"
previewBaseURL = "oktapreview.com"
)
func pathConfig(b *backend) *framework.Path { func pathConfig(b *backend) *framework.Path {
return &framework.Path{ return &framework.Path{
Pattern: `config`, Pattern: `config`,
Fields: map[string]*framework.FieldSchema{ Fields: map[string]*framework.FieldSchema{
"organization": &framework.FieldSchema{ "organization": &framework.FieldSchema{
Type: framework.TypeString, 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{ "org_name": &framework.FieldSchema{
Type: framework.TypeString, Type: framework.TypeString,
@ -27,7 +31,7 @@ func pathConfig(b *backend) *framework.Path {
}, },
"token": &framework.FieldSchema{ "token": &framework.FieldSchema{
Type: framework.TypeString, Type: framework.TypeString,
Description: "Okta admin API token (DEPRECATED)", Description: "(DEPRECATED) Okta admin API token. Use api_token instead.",
}, },
"api_token": &framework.FieldSchema{ "api_token": &framework.FieldSchema{
Type: framework.TypeString, Type: framework.TypeString,
@ -35,13 +39,11 @@ func pathConfig(b *backend) *framework.Path {
}, },
"base_url": &framework.FieldSchema{ "base_url": &framework.FieldSchema{
Type: framework.TypeString, Type: framework.TypeString,
Description: `The API endpoint to use. Useful if you Description: `The base domain to use for the Okta API. When not specified in the configuraiton, "okta.com" is used.`,
are using Okta development accounts. (DEPRECATED)`,
}, },
"production": &framework.FieldSchema{ "production": &framework.FieldSchema{
Type: framework.TypeBool, Type: framework.TypeBool,
Default: true, Description: `(DEPRECATED) Use base_url.`,
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.`,
}, },
"ttl": &framework.FieldSchema{ "ttl": &framework.FieldSchema{
Type: framework.TypeDurationSecond, Type: framework.TypeDurationSecond,
@ -100,7 +102,6 @@ func (b *backend) pathConfigRead(
Data: map[string]interface{}{ Data: map[string]interface{}{
"organization": cfg.Org, "organization": cfg.Org,
"org_name": cfg.Org, "org_name": cfg.Org,
"production": *cfg.Production,
"ttl": cfg.TTL, "ttl": cfg.TTL,
"max_ttl": cfg.MaxTTL, "max_ttl": cfg.MaxTTL,
}, },
@ -108,6 +109,9 @@ func (b *backend) pathConfigRead(
if cfg.BaseURL != "" { if cfg.BaseURL != "" {
resp.Data["base_url"] = cfg.BaseURL resp.Data["base_url"] = cfg.BaseURL
} }
if cfg.Production != nil {
resp.Data["production"] = *cfg.Production
}
return resp, nil return resp, nil
} }
@ -149,26 +153,29 @@ func (b *backend) pathConfigWrite(
cfg.Token = token.(string) 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 { if ok {
baseURLString := baseURL.(string) baseURL := baseURLRaw.(string)
if len(baseURLString) != 0 { _, err = url.Parse(fmt.Sprintf("https://%s,%s", cfg.Org, baseURL))
_, err = url.Parse(baseURLString)
if err != nil { if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error parsing given base_url: %s", err)), nil return logical.ErrorResponse(fmt.Sprintf("Error parsing given base_url: %s", err)), nil
} }
cfg.BaseURL = baseURLString cfg.BaseURL = baseURL
}
} else if req.Operation == logical.CreateOperation {
cfg.BaseURL = d.Get("base_url").(string)
} }
productionRaw := d.Get("production").(bool) // We only care about the production flag when base_url is not set. It is
cfg.Production = &productionRaw // 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") ttl, ok := d.GetOk("ttl")
if ok { if ok {
@ -207,16 +214,19 @@ func (b *backend) pathConfigExistenceCheck(
// OktaClient creates a basic okta client connection // OktaClient creates a basic okta client connection
func (c *ConfigEntry) OktaClient() *okta.Client { func (c *ConfigEntry) OktaClient() *okta.Client {
production := true baseURL := defaultBaseURL
if c.Production != nil { if c.Production != nil {
production = *c.Production if !*c.Production {
baseURL = previewBaseURL
}
} }
if c.BaseURL != "" { if c.BaseURL != "" {
if strings.Contains(c.BaseURL, "oktapreview.com") { baseURL = c.BaseURL
production = false
} }
}
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 // ConfigEntry for Okta

View File

@ -21,8 +21,9 @@ import (
const ( const (
libraryVersion = "1" libraryVersion = "1"
userAgent = "oktasdk-go/" + libraryVersion userAgent = "oktasdk-go/" + libraryVersion
productionURLFormat = "https://%s.okta.com/api/v1/" productionDomain = "okta.com"
previewProductionURLFormat = "https://%s.oktapreview.com/api/v1/" previewDomain = "oktapreview.com"
urlFormat = "https://%s.%s/api/v1/"
headerRateLimit = "X-Rate-Limit-Limit" headerRateLimit = "X-Rate-Limit-Limit"
headerRateRemaining = "X-Rate-Limit-Remaining" headerRateRemaining = "X-Rate-Limit-Remaining"
headerRateReset = "X-Rate-Limit-Reset" headerRateReset = "X-Rate-Limit-Reset"
@ -94,19 +95,38 @@ type service struct {
// NewClient returns a new OKTA API client. If a nil httpClient is // NewClient returns a new OKTA API client. If a nil httpClient is
// provided, http.DefaultClient will be used. // provided, http.DefaultClient will be used.
func NewClient(httpClient *http.Client, orgName string, apiToken string, isProduction bool) *Client { 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 { if httpClient == nil {
httpClient = http.DefaultClient httpClient = http.DefaultClient
} }
var baseURL *url.URL c := &Client{
if isProduction { client: httpClient,
baseURL, _ = url.Parse(fmt.Sprintf(productionURLFormat, orgName)) BaseURL: baseURL,
} else { UserAgent: userAgent,
baseURL, _ = url.Parse(fmt.Sprintf(previewProductionURLFormat, orgName))
} }
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.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.authorizationHeaderValue = fmt.Sprintf(headerAuthorizationFormat, apiToken)
c.apiKey = apiToken c.apiKey = apiToken

6
vendor/vendor.json vendored
View File

@ -439,10 +439,10 @@
"revisionTime": "2017-07-11T19:02:43Z" "revisionTime": "2017-07-11T19:02:43Z"
}, },
{ {
"checksumSHA1": "QZtBo/fc3zeQFxPFgPVMyDiw70M=", "checksumSHA1": "sFjc2R+KS9AeXIPMV4KCw+GwX5I=",
"path": "github.com/chrismalek/oktasdk-go/okta", "path": "github.com/chrismalek/oktasdk-go/okta",
"revision": "7d4ce0a254ec5f9eda3397523f6cf183e1d46c5e", "revision": "ae553c909ca06a4c34eb41ee435e83871a7c2496",
"revisionTime": "2017-02-07T05:01:14Z" "revisionTime": "2017-09-11T15:31:29Z"
}, },
{ {
"checksumSHA1": "WsB6y1Yd+kDbHGz1Rm7xZ44hyAE=", "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 - `org_name` `(string: <required>)` - Name of the organization to be used in the
Okta API. Okta API.
- `api_token` `(string: <required>)` - Okta API key. - `api_token` `(string: "")` - Okta API token. This is required to query Okta
- `production` `(bool: true)` - If set, production API URL prefix will be used for user group membership. If this is not supplied only locally configured
to communicate with Okta and if not set, a preview production API URL prefix groups will be enabled.
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.
- `ttl` `(string: "")` - Duration after which authentication will be expired. - `ttl` `(string: "")` - Duration after which authentication will be expired.
- `max_ttl` `(string: "")` - Maximum duration after which authentication will - `max_ttl` `(string: "")` - Maximum duration after which authentication will
be expired. be expired.
@ -83,7 +84,7 @@ $ curl \
"data": { "data": {
"org_name": "example", "org_name": "example",
"api_token": "abc123", "api_token": "abc123",
"production": true, "base_url": "okta.com",
"ttl": "", "ttl": "",
"max_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: - `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 - 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. - `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: - `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 - 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. - `org_name` `(string)` - Name of the organization to be used in the Okta API.
- `api_token` `(string)` - Okta API key. - `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 ### 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: - `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 - 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. - `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 ### Connection parameters
* `organization` (string, required) - The Okta organization. This will be the first part of the url `https://XXX.okta.com` url. * `org_name` (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 * `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` * `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. * `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) 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 \ $ 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" \ $ vault write auth/okta/config base_url="oktapreview.com" \
organization="dev-123456" \ org_name="dev-123456" \
token="00KzlTNCqDf0enpQKYSAYUt88KHqXax6dT11xEZz_g" api_token="00KzlTNCqDf0enpQKYSAYUt88KHqXax6dT11xEZz_g"
... ...
``` ```