| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package okta |
| |
| import ( |
| "context" |
| "fmt" |
| "net/http" |
| "net/url" |
| "strings" |
| "time" |
| |
| oktaold "github.com/chrismalek/oktasdk-go/okta" |
| "github.com/hashicorp/go-cleanhttp" |
| "github.com/hashicorp/vault/sdk/framework" |
| "github.com/hashicorp/vault/sdk/helper/tokenutil" |
| "github.com/hashicorp/vault/sdk/logical" |
| oktanew "github.com/okta/okta-sdk-golang/v2/okta" |
| ) |
| |
| const ( |
| defaultBaseURL = "okta.com" |
| previewBaseURL = "oktapreview.com" |
| ) |
| |
| func pathConfig(b *backend) *framework.Path { |
| p := &framework.Path{ |
| Pattern: `config`, |
| |
| DisplayAttrs: &framework.DisplayAttributes{ |
| OperationPrefix: operationPrefixOkta, |
| Action: "Configure", |
| }, |
| |
| Fields: map[string]*framework.FieldSchema{ |
| "organization": { |
| Type: framework.TypeString, |
| Description: "Use org_name instead.", |
| Deprecated: true, |
| }, |
| "org_name": { |
| Type: framework.TypeString, |
| Description: "Name of the organization to be used in the Okta API.", |
| DisplayAttrs: &framework.DisplayAttributes{ |
| Name: "Organization Name", |
| }, |
| }, |
| "token": { |
| Type: framework.TypeString, |
| Description: "Use api_token instead.", |
| Deprecated: true, |
| }, |
| "api_token": { |
| Type: framework.TypeString, |
| Description: "Okta API key.", |
| DisplayAttrs: &framework.DisplayAttributes{ |
| Name: "API Token", |
| }, |
| }, |
| "base_url": { |
| Type: framework.TypeString, |
| Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`, |
| DisplayAttrs: &framework.DisplayAttributes{ |
| Name: "Base URL", |
| }, |
| }, |
| "production": { |
| Type: framework.TypeBool, |
| Description: `Use base_url instead.`, |
| Deprecated: true, |
| }, |
| "ttl": { |
| Type: framework.TypeDurationSecond, |
| Description: tokenutil.DeprecationText("token_ttl"), |
| Deprecated: true, |
| }, |
| "max_ttl": { |
| Type: framework.TypeDurationSecond, |
| Description: tokenutil.DeprecationText("token_max_ttl"), |
| Deprecated: true, |
| }, |
| "bypass_okta_mfa": { |
| Type: framework.TypeBool, |
| Description: `When set true, requests by Okta for a MFA check will be bypassed. This also disallows certain status checks on the account, such as whether the password is expired.`, |
| DisplayAttrs: &framework.DisplayAttributes{ |
| Name: "Bypass Okta MFA", |
| }, |
| }, |
| }, |
| |
| Operations: map[logical.Operation]framework.OperationHandler{ |
| logical.ReadOperation: &framework.PathOperation{ |
| Callback: b.pathConfigRead, |
| DisplayAttrs: &framework.DisplayAttributes{ |
| OperationSuffix: "configuration", |
| }, |
| }, |
| logical.CreateOperation: &framework.PathOperation{ |
| Callback: b.pathConfigWrite, |
| DisplayAttrs: &framework.DisplayAttributes{ |
| OperationVerb: "configure", |
| }, |
| }, |
| logical.UpdateOperation: &framework.PathOperation{ |
| Callback: b.pathConfigWrite, |
| DisplayAttrs: &framework.DisplayAttributes{ |
| OperationVerb: "configure", |
| }, |
| }, |
| }, |
| |
| ExistenceCheck: b.pathConfigExistenceCheck, |
| |
| HelpSynopsis: pathConfigHelp, |
| } |
| |
| tokenutil.AddTokenFields(p.Fields) |
| p.Fields["token_policies"].Description += ". This will apply to all tokens generated by this auth method, in addition to any configured for specific users/groups." |
| return p |
| } |
| |
| // Config returns the configuration for this backend. |
| func (b *backend) Config(ctx context.Context, s logical.Storage) (*ConfigEntry, error) { |
| entry, err := s.Get(ctx, "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 |
| } |
| } |
| |
| if result.TokenTTL == 0 && result.TTL > 0 { |
| result.TokenTTL = result.TTL |
| } |
| if result.TokenMaxTTL == 0 && result.MaxTTL > 0 { |
| result.TokenMaxTTL = result.MaxTTL |
| } |
| |
| return &result, nil |
| } |
| |
| func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
| cfg, err := b.Config(ctx, req.Storage) |
| if err != nil { |
| return nil, err |
| } |
| if cfg == nil { |
| return nil, nil |
| } |
| |
| data := map[string]interface{}{ |
| "organization": cfg.Org, |
| "org_name": cfg.Org, |
| "bypass_okta_mfa": cfg.BypassOktaMFA, |
| } |
| cfg.PopulateTokenData(data) |
| |
| if cfg.BaseURL != "" { |
| data["base_url"] = cfg.BaseURL |
| } |
| if cfg.Production != nil { |
| data["production"] = *cfg.Production |
| } |
| if cfg.TTL > 0 { |
| data["ttl"] = int64(cfg.TTL.Seconds()) |
| } |
| if cfg.MaxTTL > 0 { |
| data["max_ttl"] = int64(cfg.MaxTTL.Seconds()) |
| } |
| |
| resp := &logical.Response{ |
| Data: data, |
| } |
| |
| if cfg.BypassOktaMFA { |
| resp.AddWarning("Okta MFA bypass is configured. In addition to ignoring Okta MFA requests, certain other account statuses will not be seen, such as PASSWORD_EXPIRED. Authentication will succeed in these cases.") |
| } |
| |
| return resp, nil |
| } |
| |
| func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
| cfg, err := b.Config(ctx, 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, ok := d.GetOk("org_name") |
| if ok { |
| cfg.Org = org.(string) |
| } |
| if cfg.Org == "" { |
| org, ok = d.GetOk("organization") |
| if ok { |
| cfg.Org = org.(string) |
| } |
| } |
| if cfg.Org == "" && req.Operation == logical.CreateOperation { |
| return logical.ErrorResponse("org_name is missing"), nil |
| } |
| |
| token, ok := d.GetOk("api_token") |
| if ok { |
| cfg.Token = token.(string) |
| } else if token, ok = d.GetOk("token"); ok { |
| cfg.Token = token.(string) |
| } |
| |
| baseURLRaw, ok := d.GetOk("base_url") |
| if ok { |
| 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 = baseURL |
| } |
| |
| // 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 |
| } |
| |
| bypass, ok := d.GetOk("bypass_okta_mfa") |
| if ok { |
| cfg.BypassOktaMFA = bypass.(bool) |
| } |
| |
| if err := cfg.ParseTokenFields(req, d); err != nil { |
| return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest |
| } |
| |
| // Handle upgrade cases |
| { |
| if err := tokenutil.UpgradeValue(d, "ttl", "token_ttl", &cfg.TTL, &cfg.TokenTTL); err != nil { |
| return logical.ErrorResponse(err.Error()), nil |
| } |
| |
| if err := tokenutil.UpgradeValue(d, "max_ttl", "token_max_ttl", &cfg.MaxTTL, &cfg.TokenMaxTTL); err != nil { |
| return logical.ErrorResponse(err.Error()), nil |
| } |
| } |
| |
| jsonCfg, err := logical.StorageEntryJSON("config", cfg) |
| if err != nil { |
| return nil, err |
| } |
| if err := req.Storage.Put(ctx, jsonCfg); err != nil { |
| return nil, err |
| } |
| |
| var resp *logical.Response |
| if cfg.BypassOktaMFA { |
| resp = new(logical.Response) |
| resp.AddWarning("Okta MFA bypass is configured. In addition to ignoring Okta MFA requests, certain other account statuses will not be seen, such as PASSWORD_EXPIRED. Authentication will succeed in these cases.") |
| } |
| |
| return resp, nil |
| } |
| |
| func (b *backend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) { |
| cfg, err := b.Config(ctx, req.Storage) |
| if err != nil { |
| return false, err |
| } |
| |
| return cfg != nil, nil |
| } |
| |
| type oktaShim interface { |
| Client() (*oktanew.Client, context.Context) |
| NewRequest(method string, url string, body interface{}) (*http.Request, error) |
| Do(req *http.Request, v interface{}) (interface{}, error) |
| } |
| |
| type oktaShimNew struct { |
| client *oktanew.Client |
| ctx context.Context |
| } |
| |
| func (new *oktaShimNew) Client() (*oktanew.Client, context.Context) { |
| return new.client, new.ctx |
| } |
| |
| func (new *oktaShimNew) NewRequest(method string, url string, body interface{}) (*http.Request, error) { |
| if !strings.HasPrefix(url, "/") { |
| url = "/api/v1/" + url |
| } |
| return new.client.GetRequestExecutor().NewRequest(method, url, body) |
| } |
| |
| func (new *oktaShimNew) Do(req *http.Request, v interface{}) (interface{}, error) { |
| return new.client.GetRequestExecutor().Do(new.ctx, req, v) |
| } |
| |
| type oktaShimOld struct { |
| client *oktaold.Client |
| } |
| |
| func (new *oktaShimOld) Client() (*oktanew.Client, context.Context) { |
| return nil, nil |
| } |
| |
| func (new *oktaShimOld) NewRequest(method string, url string, body interface{}) (*http.Request, error) { |
| return new.client.NewRequest(method, url, body) |
| } |
| |
| func (new *oktaShimOld) Do(req *http.Request, v interface{}) (interface{}, error) { |
| return new.client.Do(req, v) |
| } |
| |
| // OktaClient creates a basic okta client connection |
| func (c *ConfigEntry) OktaClient(ctx context.Context) (oktaShim, error) { |
| baseURL := defaultBaseURL |
| if c.Production != nil { |
| if !*c.Production { |
| baseURL = previewBaseURL |
| } |
| } |
| if c.BaseURL != "" { |
| baseURL = c.BaseURL |
| } |
| |
| if c.Token != "" { |
| ctx, client, err := oktanew.NewClient(ctx, |
| oktanew.WithOrgUrl("https://"+c.Org+"."+baseURL), |
| oktanew.WithToken(c.Token)) |
| if err != nil { |
| return nil, err |
| } |
| return &oktaShimNew{client, ctx}, nil |
| } |
| client, err := oktaold.NewClientWithDomain(cleanhttp.DefaultClient(), c.Org, baseURL, "") |
| if err != nil { |
| return nil, err |
| } |
| return &oktaShimOld{client}, nil |
| } |
| |
| // ConfigEntry for Okta |
| type ConfigEntry struct { |
| tokenutil.TokenParams |
| |
| Org string `json:"organization"` |
| Token string `json:"token"` |
| BaseURL string `json:"base_url"` |
| Production *bool `json:"is_production,omitempty"` |
| TTL time.Duration `json:"ttl"` |
| MaxTTL time.Duration `json:"max_ttl"` |
| BypassOktaMFA bool `json:"bypass_okta_mfa"` |
| } |
| |
| 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 |
| ` |