From 3aa68c00347ad74a0a4dc1c6e48164643b151bcc Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Fri, 15 Sep 2017 00:27:45 -0400 Subject: [PATCH] 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 --- builtin/credential/okta/backend.go | 55 ++++++++------ builtin/credential/okta/path_config.go | 72 +++++++++++-------- .../chrismalek/oktasdk-go/okta/sdk.go | 60 ++++++++++------ vendor/vendor.json | 6 +- website/source/api/auth/okta/index.html.md | 11 +-- website/source/api/system/mfa-duo.html.md | 3 + website/source/api/system/mfa-okta.html.md | 5 +- website/source/api/system/mfa-pingid.html.md | 3 + website/source/docs/auth/okta.html.md | 10 +-- 9 files changed, 137 insertions(+), 88 deletions(-) diff --git a/builtin/credential/okta/backend.go b/builtin/credential/okta/backend.go index 969fd42721..951d190973 100644 --- a/builtin/credential/okta/backend.go +++ b/builtin/credential/okta/backend.go @@ -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 { @@ -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 diff --git a/builtin/credential/okta/path_config.go b/builtin/credential/okta/path_config.go index 19014eddf7..e879302242 100644 --- a/builtin/credential/okta/path_config.go +++ b/builtin/credential/okta/path_config.go @@ -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,21 +31,19 @@ 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, 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, @@ -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) - 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 { @@ -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.BaseURL != "" { - if strings.Contains(c.BaseURL, "oktapreview.com") { - production = false + if !*c.Production { + baseURL = previewBaseURL } } - return okta.NewClient(cleanhttp.DefaultClient(), c.Org, c.Token, production) + if c.BaseURL != "" { + baseURL = c.BaseURL + } + + // 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 diff --git a/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go b/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go index 322b4410d4..d8ec24d6ef 100644 --- a/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go +++ b/vendor/github.com/chrismalek/oktasdk-go/okta/sdk.go @@ -19,18 +19,19 @@ import ( ) const ( - libraryVersion = "1" - userAgent = "oktasdk-go/" + libraryVersion - productionURLFormat = "https://%s.okta.com/api/v1/" - previewProductionURLFormat = "https://%s.oktapreview.com/api/v1/" - headerRateLimit = "X-Rate-Limit-Limit" - headerRateRemaining = "X-Rate-Limit-Remaining" - headerRateReset = "X-Rate-Limit-Reset" - headerOKTARequestID = "X-Okta-Request-Id" - headerAuthorization = "Authorization" - headerAuthorizationFormat = "SSWS %v" - mediaTypeJSON = "application/json" - defaultLimit = 50 + libraryVersion = "1" + userAgent = "oktasdk-go/" + libraryVersion + 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" + headerOKTARequestID = "X-Okta-Request-Id" + headerAuthorization = "Authorization" + headerAuthorizationFormat = "SSWS %v" + mediaTypeJSON = "application/json" + defaultLimit = 50 // FilterEqualOperator Filter Operatorid for "equal" FilterEqualOperator = "eq" // FilterStartsWithOperator - filter operator for "starts with" @@ -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 diff --git a/vendor/vendor.json b/vendor/vendor.json index 425a398c7f..7e6ed417ca 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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=", diff --git a/website/source/api/auth/okta/index.html.md b/website/source/api/auth/okta/index.html.md index 2bd22b2042..cff51af943 100644 --- a/website/source/api/auth/okta/index.html.md +++ b/website/source/api/auth/okta/index.html.md @@ -29,10 +29,11 @@ distinction between the `create` and `update` capabilities inside ACL policies. - `org_name` `(string: )` - Name of the organization to be used in the Okta API. -- `api_token` `(string: )` - 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": "" }, diff --git a/website/source/api/system/mfa-duo.html.md b/website/source/api/system/mfa-duo.html.md index 31b0850475..db081fbca0 100644 --- a/website/source/api/system/mfa-duo.html.md +++ b/website/source/api/system/mfa-duo.html.md @@ -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.``: The value of the Persona's metadata parameter + - entity.metadata.``: The value of the Entity's metadata paramater - `secret_key` `(string)` - Secret key for Duo. diff --git a/website/source/api/system/mfa-okta.html.md b/website/source/api/system/mfa-okta.html.md index 728c0dadf1..1b82370eb6 100644 --- a/website/source/api/system/mfa-okta.html.md +++ b/website/source/api/system/mfa-okta.html.md @@ -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.``: The value of the Persona's metadata parameter + - entity.metadata.``: 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 diff --git a/website/source/api/system/mfa-pingid.html.md b/website/source/api/system/mfa-pingid.html.md index 59b75ed3d6..a519f87ee1 100644 --- a/website/source/api/system/mfa-pingid.html.md +++ b/website/source/api/system/mfa-pingid.html.md @@ -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.``: The value of the Persona's metadata parameter + - entity.metadata.``: 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. diff --git a/website/source/docs/auth/okta.html.md b/website/source/docs/auth/okta.html.md index 4e4ce47a7f..539c35118d 100644 --- a/website/source/docs/auth/okta.html.md +++ b/website/source/docs/auth/okta.html.md @@ -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" ... ```