package okta import ( "context" "fmt" "net/url" "time" "github.com/chrismalek/oktasdk-go/okta" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/vault/logical" "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: "(DEPRECATED) Okta organization to authenticate against. Use org_name instead.", }, "org_name": &framework.FieldSchema{ Type: framework.TypeString, Description: "Name of the organization to be used in the Okta API.", }, "token": &framework.FieldSchema{ Type: framework.TypeString, 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 base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`, }, "production": &framework.FieldSchema{ Type: framework.TypeBool, Description: `(DEPRECATED) Use base_url.`, }, "ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Description: `Duration after which authentication will be expired`, }, "max_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Description: `Maximum duration after which authentication will be expired`, }, "bypass_okta_mfa": &framework.FieldSchema{ 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.`, }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.pathConfigRead, logical.CreateOperation: b.pathConfigWrite, logical.UpdateOperation: b.pathConfigWrite, }, ExistenceCheck: b.pathConfigExistenceCheck, HelpSynopsis: pathConfigHelp, } } // 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 } } 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 } resp := &logical.Response{ Data: map[string]interface{}{ "organization": cfg.Org, "org_name": cfg.Org, "ttl": cfg.TTL.Seconds(), "max_ttl": cfg.MaxTTL.Seconds(), "bypass_okta_mfa": cfg.BypassOktaMFA, }, } if cfg.BaseURL != "" { resp.Data["base_url"] = cfg.BaseURL } if cfg.Production != nil { resp.Data["production"] = *cfg.Production } 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) } ttl, ok := d.GetOk("ttl") if ok { cfg.TTL = time.Duration(ttl.(int)) * time.Second } else if req.Operation == logical.CreateOperation { cfg.TTL = time.Duration(d.Get("ttl").(int)) * time.Second } maxTTL, ok := d.GetOk("max_ttl") if ok { cfg.MaxTTL = time.Duration(maxTTL.(int)) * time.Second } else if req.Operation == logical.CreateOperation { cfg.MaxTTL = time.Duration(d.Get("max_ttl").(int)) * time.Second } 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 } // OktaClient creates a basic okta client connection func (c *ConfigEntry) OktaClient() *okta.Client { baseURL := defaultBaseURL if c.Production != nil { if !*c.Production { baseURL = previewBaseURL } } 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 type ConfigEntry struct { 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 `