1package okta 2 3import ( 4 "context" 5 "fmt" 6 "net/url" 7 8 "time" 9 10 "github.com/chrismalek/oktasdk-go/okta" 11 cleanhttp "github.com/hashicorp/go-cleanhttp" 12 "github.com/hashicorp/vault/sdk/framework" 13 "github.com/hashicorp/vault/sdk/helper/tokenutil" 14 "github.com/hashicorp/vault/sdk/logical" 15) 16 17const ( 18 defaultBaseURL = "okta.com" 19 previewBaseURL = "oktapreview.com" 20) 21 22func pathConfig(b *backend) *framework.Path { 23 p := &framework.Path{ 24 Pattern: `config`, 25 Fields: map[string]*framework.FieldSchema{ 26 "organization": &framework.FieldSchema{ 27 Type: framework.TypeString, 28 Description: "Use org_name instead.", 29 Deprecated: true, 30 }, 31 "org_name": &framework.FieldSchema{ 32 Type: framework.TypeString, 33 Description: "Name of the organization to be used in the Okta API.", 34 DisplayAttrs: &framework.DisplayAttributes{ 35 Name: "Organization Name", 36 }, 37 }, 38 "token": &framework.FieldSchema{ 39 Type: framework.TypeString, 40 Description: "Use api_token instead.", 41 Deprecated: true, 42 }, 43 "api_token": &framework.FieldSchema{ 44 Type: framework.TypeString, 45 Description: "Okta API key.", 46 DisplayAttrs: &framework.DisplayAttributes{ 47 Name: "API Token", 48 }, 49 }, 50 "base_url": &framework.FieldSchema{ 51 Type: framework.TypeString, 52 Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`, 53 DisplayAttrs: &framework.DisplayAttributes{ 54 Name: "Base URL", 55 }, 56 }, 57 "production": &framework.FieldSchema{ 58 Type: framework.TypeBool, 59 Description: `Use base_url instead.`, 60 Deprecated: true, 61 }, 62 "ttl": &framework.FieldSchema{ 63 Type: framework.TypeDurationSecond, 64 Description: tokenutil.DeprecationText("token_ttl"), 65 Deprecated: true, 66 }, 67 "max_ttl": &framework.FieldSchema{ 68 Type: framework.TypeDurationSecond, 69 Description: tokenutil.DeprecationText("token_max_ttl"), 70 Deprecated: true, 71 }, 72 "bypass_okta_mfa": &framework.FieldSchema{ 73 Type: framework.TypeBool, 74 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.`, 75 DisplayAttrs: &framework.DisplayAttributes{ 76 Name: "Bypass Okta MFA", 77 }, 78 }, 79 }, 80 81 Callbacks: map[logical.Operation]framework.OperationFunc{ 82 logical.ReadOperation: b.pathConfigRead, 83 logical.CreateOperation: b.pathConfigWrite, 84 logical.UpdateOperation: b.pathConfigWrite, 85 }, 86 87 ExistenceCheck: b.pathConfigExistenceCheck, 88 89 HelpSynopsis: pathConfigHelp, 90 } 91 92 tokenutil.AddTokenFields(p.Fields) 93 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." 94 return p 95} 96 97// Config returns the configuration for this backend. 98func (b *backend) Config(ctx context.Context, s logical.Storage) (*ConfigEntry, error) { 99 entry, err := s.Get(ctx, "config") 100 if err != nil { 101 return nil, err 102 } 103 if entry == nil { 104 return nil, nil 105 } 106 107 var result ConfigEntry 108 if entry != nil { 109 if err := entry.DecodeJSON(&result); err != nil { 110 return nil, err 111 } 112 } 113 114 if result.TokenTTL == 0 && result.TTL > 0 { 115 result.TokenTTL = result.TTL 116 } 117 if result.TokenMaxTTL == 0 && result.MaxTTL > 0 { 118 result.TokenMaxTTL = result.MaxTTL 119 } 120 121 return &result, nil 122} 123 124func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 125 cfg, err := b.Config(ctx, req.Storage) 126 if err != nil { 127 return nil, err 128 } 129 if cfg == nil { 130 return nil, nil 131 } 132 133 data := map[string]interface{}{ 134 "organization": cfg.Org, 135 "org_name": cfg.Org, 136 "bypass_okta_mfa": cfg.BypassOktaMFA, 137 } 138 cfg.PopulateTokenData(data) 139 140 if cfg.BaseURL != "" { 141 data["base_url"] = cfg.BaseURL 142 } 143 if cfg.Production != nil { 144 data["production"] = *cfg.Production 145 } 146 if cfg.TTL > 0 { 147 data["ttl"] = int64(cfg.TTL.Seconds()) 148 } 149 if cfg.MaxTTL > 0 { 150 data["max_ttl"] = int64(cfg.MaxTTL.Seconds()) 151 } 152 153 resp := &logical.Response{ 154 Data: data, 155 } 156 157 if cfg.BypassOktaMFA { 158 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.") 159 } 160 161 return resp, nil 162} 163 164func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 165 cfg, err := b.Config(ctx, req.Storage) 166 if err != nil { 167 return nil, err 168 } 169 170 // Due to the existence check, entry will only be nil if it's a create 171 // operation, so just create a new one 172 if cfg == nil { 173 cfg = &ConfigEntry{} 174 } 175 176 org, ok := d.GetOk("org_name") 177 if ok { 178 cfg.Org = org.(string) 179 } 180 if cfg.Org == "" { 181 org, ok = d.GetOk("organization") 182 if ok { 183 cfg.Org = org.(string) 184 } 185 } 186 if cfg.Org == "" && req.Operation == logical.CreateOperation { 187 return logical.ErrorResponse("org_name is missing"), nil 188 } 189 190 token, ok := d.GetOk("api_token") 191 if ok { 192 cfg.Token = token.(string) 193 } else if token, ok = d.GetOk("token"); ok { 194 cfg.Token = token.(string) 195 } 196 197 baseURLRaw, ok := d.GetOk("base_url") 198 if ok { 199 baseURL := baseURLRaw.(string) 200 _, err = url.Parse(fmt.Sprintf("https://%s,%s", cfg.Org, baseURL)) 201 if err != nil { 202 return logical.ErrorResponse(fmt.Sprintf("Error parsing given base_url: %s", err)), nil 203 } 204 cfg.BaseURL = baseURL 205 } 206 207 // We only care about the production flag when base_url is not set. It is 208 // for compatibility reasons. 209 if cfg.BaseURL == "" { 210 productionRaw, ok := d.GetOk("production") 211 if ok { 212 production := productionRaw.(bool) 213 cfg.Production = &production 214 } 215 } else { 216 // clear out old production flag if base_url is set 217 cfg.Production = nil 218 } 219 220 bypass, ok := d.GetOk("bypass_okta_mfa") 221 if ok { 222 cfg.BypassOktaMFA = bypass.(bool) 223 } 224 225 if err := cfg.ParseTokenFields(req, d); err != nil { 226 return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest 227 } 228 229 // Handle upgrade cases 230 { 231 if err := tokenutil.UpgradeValue(d, "ttl", "token_ttl", &cfg.TTL, &cfg.TokenTTL); err != nil { 232 return logical.ErrorResponse(err.Error()), nil 233 } 234 235 if err := tokenutil.UpgradeValue(d, "max_ttl", "token_max_ttl", &cfg.MaxTTL, &cfg.TokenMaxTTL); err != nil { 236 return logical.ErrorResponse(err.Error()), nil 237 } 238 } 239 240 jsonCfg, err := logical.StorageEntryJSON("config", cfg) 241 if err != nil { 242 return nil, err 243 } 244 if err := req.Storage.Put(ctx, jsonCfg); err != nil { 245 return nil, err 246 } 247 248 var resp *logical.Response 249 if cfg.BypassOktaMFA { 250 resp = new(logical.Response) 251 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.") 252 } 253 254 return resp, nil 255} 256 257func (b *backend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, d *framework.FieldData) (bool, error) { 258 cfg, err := b.Config(ctx, req.Storage) 259 if err != nil { 260 return false, err 261 } 262 263 return cfg != nil, nil 264} 265 266// OktaClient creates a basic okta client connection 267func (c *ConfigEntry) OktaClient() *okta.Client { 268 baseURL := defaultBaseURL 269 if c.Production != nil { 270 if !*c.Production { 271 baseURL = previewBaseURL 272 } 273 } 274 if c.BaseURL != "" { 275 baseURL = c.BaseURL 276 } 277 278 // We validate config on input and errors are only returned when parsing URLs 279 client, _ := okta.NewClientWithDomain(cleanhttp.DefaultClient(), c.Org, baseURL, c.Token) 280 return client 281} 282 283// ConfigEntry for Okta 284type ConfigEntry struct { 285 tokenutil.TokenParams 286 287 Org string `json:"organization"` 288 Token string `json:"token"` 289 BaseURL string `json:"base_url"` 290 Production *bool `json:"is_production,omitempty"` 291 TTL time.Duration `json:"ttl"` 292 MaxTTL time.Duration `json:"max_ttl"` 293 BypassOktaMFA bool `json:"bypass_okta_mfa"` 294} 295 296const pathConfigHelp = ` 297This endpoint allows you to configure the Okta and its 298configuration options. 299 300The Okta organization are the characters at the front of the URL for Okta. 301Example https://ORG.okta.com 302` 303