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`, 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", }, }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.pathConfigRead, logical.CreateOperation: b.pathConfigWrite, logical.UpdateOperation: b.pathConfigWrite, }, ExistenceCheck: b.pathConfigExistenceCheck, HelpSynopsis: pathConfigHelp, DisplayAttrs: &framework.DisplayAttributes{ Action: "Configure", }, } 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 `