1package jwtauth 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/hashicorp/go-sockaddr" 11 "github.com/hashicorp/vault/sdk/framework" 12 "github.com/hashicorp/vault/sdk/helper/strutil" 13 "github.com/hashicorp/vault/sdk/helper/tokenutil" 14 "github.com/hashicorp/vault/sdk/logical" 15 "gopkg.in/square/go-jose.v2/jwt" 16) 17 18var reservedMetadata = []string{"role"} 19 20const ( 21 claimDefaultLeeway = 150 22 boundClaimsTypeString = "string" 23 boundClaimsTypeGlob = "glob" 24) 25 26func pathRoleList(b *jwtAuthBackend) *framework.Path { 27 return &framework.Path{ 28 Pattern: "role/?", 29 Operations: map[logical.Operation]framework.OperationHandler{ 30 logical.ListOperation: &framework.PathOperation{ 31 Callback: b.pathRoleList, 32 Summary: strings.TrimSpace(roleHelp["role-list"][0]), 33 Description: strings.TrimSpace(roleHelp["role-list"][1]), 34 }, 35 }, 36 HelpSynopsis: strings.TrimSpace(roleHelp["role-list"][0]), 37 HelpDescription: strings.TrimSpace(roleHelp["role-list"][1]), 38 } 39} 40 41// pathRole returns the path configurations for the CRUD operations on roles 42func pathRole(b *jwtAuthBackend) *framework.Path { 43 p := &framework.Path{ 44 Pattern: "role/" + framework.GenericNameRegex("name"), 45 Fields: map[string]*framework.FieldSchema{ 46 "name": { 47 Type: framework.TypeLowerCaseString, 48 Description: "Name of the role.", 49 }, 50 "role_type": { 51 Type: framework.TypeString, 52 Description: "Type of the role, either 'jwt' or 'oidc'.", 53 }, 54 55 "policies": { 56 Type: framework.TypeCommaStringSlice, 57 Description: tokenutil.DeprecationText("token_policies"), 58 Deprecated: true, 59 }, 60 "num_uses": { 61 Type: framework.TypeInt, 62 Description: tokenutil.DeprecationText("token_num_uses"), 63 Deprecated: true, 64 }, 65 "ttl": { 66 Type: framework.TypeDurationSecond, 67 Description: tokenutil.DeprecationText("token_ttl"), 68 Deprecated: true, 69 }, 70 "max_ttl": { 71 Type: framework.TypeDurationSecond, 72 Description: tokenutil.DeprecationText("token_max_ttl"), 73 Deprecated: true, 74 }, 75 "period": { 76 Type: framework.TypeDurationSecond, 77 Description: tokenutil.DeprecationText("token_period"), 78 Deprecated: true, 79 }, 80 "bound_cidrs": { 81 Type: framework.TypeCommaStringSlice, 82 Description: tokenutil.DeprecationText("token_bound_cidrs"), 83 Deprecated: true, 84 }, 85 "expiration_leeway": { 86 Type: framework.TypeSignedDurationSecond, 87 Description: `Duration in seconds of leeway when validating expiration of a token to account for clock skew. 88Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`, 89 Default: claimDefaultLeeway, 90 }, 91 "not_before_leeway": { 92 Type: framework.TypeSignedDurationSecond, 93 Description: `Duration in seconds of leeway when validating not before values of a token to account for clock skew. 94Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`, 95 Default: claimDefaultLeeway, 96 }, 97 "clock_skew_leeway": { 98 Type: framework.TypeSignedDurationSecond, 99 Description: `Duration in seconds of leeway when validating all claims to account for clock skew. 100Defaults to 60 (1 minute) if set to 0 and can be disabled if set to -1.`, 101 Default: jwt.DefaultLeeway, 102 }, 103 "bound_subject": { 104 Type: framework.TypeString, 105 Description: `The 'sub' claim that is valid for login. Optional.`, 106 }, 107 "bound_audiences": { 108 Type: framework.TypeCommaStringSlice, 109 Description: `Comma-separated list of 'aud' claims that are valid for login; any match is sufficient`, 110 }, 111 "bound_claims_type": { 112 Type: framework.TypeString, 113 Description: `How to interpret values in the map of claims/values (which must match for login): allowed values are 'string' or 'glob'`, 114 Default: boundClaimsTypeString, 115 }, 116 "bound_claims": { 117 Type: framework.TypeMap, 118 Description: `Map of claims/values which must match for login`, 119 }, 120 "claim_mappings": { 121 Type: framework.TypeKVPairs, 122 Description: `Mappings of claims (key) that will be copied to a metadata field (value)`, 123 }, 124 "user_claim": { 125 Type: framework.TypeString, 126 Description: `The claim to use for the Identity entity alias name`, 127 }, 128 "groups_claim": { 129 Type: framework.TypeString, 130 Description: `The claim to use for the Identity group alias names`, 131 }, 132 "oidc_scopes": { 133 Type: framework.TypeCommaStringSlice, 134 Description: `Comma-separated list of OIDC scopes`, 135 }, 136 "allowed_redirect_uris": { 137 Type: framework.TypeCommaStringSlice, 138 Description: `Comma-separated list of allowed values for redirect_uri`, 139 }, 140 "verbose_oidc_logging": { 141 Type: framework.TypeBool, 142 Description: `Log received OIDC tokens and claims when debug-level logging is active. 143Not recommended in production since sensitive information may be present 144in OIDC responses.`, 145 }, 146 "max_age": { 147 Type: framework.TypeDurationSecond, 148 Description: `Specifies the allowable elapsed time in seconds since the last time the 149user was actively authenticated.`, 150 }, 151 }, 152 ExistenceCheck: b.pathRoleExistenceCheck, 153 Operations: map[logical.Operation]framework.OperationHandler{ 154 logical.ReadOperation: &framework.PathOperation{ 155 Callback: b.pathRoleRead, 156 Summary: "Read an existing role.", 157 }, 158 159 logical.UpdateOperation: &framework.PathOperation{ 160 Callback: b.pathRoleCreateUpdate, 161 Summary: strings.TrimSpace(roleHelp["role"][0]), 162 Description: strings.TrimSpace(roleHelp["role"][1]), 163 }, 164 165 logical.CreateOperation: &framework.PathOperation{ 166 Callback: b.pathRoleCreateUpdate, 167 Summary: strings.TrimSpace(roleHelp["role"][0]), 168 Description: strings.TrimSpace(roleHelp["role"][1]), 169 }, 170 171 logical.DeleteOperation: &framework.PathOperation{ 172 Callback: b.pathRoleDelete, 173 Summary: "Delete an existing role.", 174 }, 175 }, 176 HelpSynopsis: strings.TrimSpace(roleHelp["role"][0]), 177 HelpDescription: strings.TrimSpace(roleHelp["role"][1]), 178 } 179 180 tokenutil.AddTokenFields(p.Fields) 181 return p 182} 183 184type jwtRole struct { 185 tokenutil.TokenParams 186 187 RoleType string `json:"role_type"` 188 189 // Duration of leeway for expiration to account for clock skew 190 ExpirationLeeway time.Duration `json:"expiration_leeway"` 191 192 // Duration of leeway for not before to account for clock skew 193 NotBeforeLeeway time.Duration `json:"not_before_leeway"` 194 195 // Duration of leeway for all claims to account for clock skew 196 ClockSkewLeeway time.Duration `json:"clock_skew_leeway"` 197 198 // Role binding properties 199 BoundAudiences []string `json:"bound_audiences"` 200 BoundSubject string `json:"bound_subject"` 201 BoundClaimsType string `json:"bound_claims_type"` 202 BoundClaims map[string]interface{} `json:"bound_claims"` 203 ClaimMappings map[string]string `json:"claim_mappings"` 204 UserClaim string `json:"user_claim"` 205 GroupsClaim string `json:"groups_claim"` 206 OIDCScopes []string `json:"oidc_scopes"` 207 AllowedRedirectURIs []string `json:"allowed_redirect_uris"` 208 VerboseOIDCLogging bool `json:"verbose_oidc_logging"` 209 MaxAge time.Duration `json:"max_age"` 210 211 // Deprecated by TokenParams 212 Policies []string `json:"policies"` 213 NumUses int `json:"num_uses"` 214 TTL time.Duration `json:"ttl"` 215 MaxTTL time.Duration `json:"max_ttl"` 216 Period time.Duration `json:"period"` 217 BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"` 218} 219 220// role takes a storage backend and the name and returns the role's storage 221// entry 222func (b *jwtAuthBackend) role(ctx context.Context, s logical.Storage, name string) (*jwtRole, error) { 223 raw, err := s.Get(ctx, rolePrefix+name) 224 if err != nil { 225 return nil, err 226 } 227 if raw == nil { 228 return nil, nil 229 } 230 231 role := new(jwtRole) 232 if err := raw.DecodeJSON(role); err != nil { 233 return nil, err 234 } 235 236 // Report legacy roles as type "jwt" 237 if role.RoleType == "" { 238 role.RoleType = "jwt" 239 } 240 241 if role.BoundClaimsType == "" { 242 role.BoundClaimsType = boundClaimsTypeString 243 } 244 245 if role.TokenTTL == 0 && role.TTL > 0 { 246 role.TokenTTL = role.TTL 247 } 248 if role.TokenMaxTTL == 0 && role.MaxTTL > 0 { 249 role.TokenMaxTTL = role.MaxTTL 250 } 251 if role.TokenPeriod == 0 && role.Period > 0 { 252 role.TokenPeriod = role.Period 253 } 254 if role.TokenNumUses == 0 && role.NumUses > 0 { 255 role.TokenNumUses = role.NumUses 256 } 257 if len(role.TokenPolicies) == 0 && len(role.Policies) > 0 { 258 role.TokenPolicies = role.Policies 259 } 260 if len(role.TokenBoundCIDRs) == 0 && len(role.BoundCIDRs) > 0 { 261 role.TokenBoundCIDRs = role.BoundCIDRs 262 } 263 264 return role, nil 265} 266 267// pathRoleExistenceCheck returns whether the role with the given name exists or not. 268func (b *jwtAuthBackend) pathRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 269 role, err := b.role(ctx, req.Storage, data.Get("name").(string)) 270 if err != nil { 271 return false, err 272 } 273 return role != nil, nil 274} 275 276// pathRoleList is used to list all the Roles registered with the backend. 277func (b *jwtAuthBackend) pathRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 278 roles, err := req.Storage.List(ctx, rolePrefix) 279 if err != nil { 280 return nil, err 281 } 282 return logical.ListResponse(roles), nil 283} 284 285// pathRoleRead grabs a read lock and reads the options set on the role from the storage 286func (b *jwtAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 287 roleName := data.Get("name").(string) 288 if roleName == "" { 289 return logical.ErrorResponse("missing name"), nil 290 } 291 292 role, err := b.role(ctx, req.Storage, roleName) 293 if err != nil { 294 return nil, err 295 } 296 if role == nil { 297 return nil, nil 298 } 299 300 // Create a map of data to be returned 301 d := map[string]interface{}{ 302 "role_type": role.RoleType, 303 "expiration_leeway": int64(role.ExpirationLeeway.Seconds()), 304 "not_before_leeway": int64(role.NotBeforeLeeway.Seconds()), 305 "clock_skew_leeway": int64(role.ClockSkewLeeway.Seconds()), 306 "bound_audiences": role.BoundAudiences, 307 "bound_subject": role.BoundSubject, 308 "bound_claims_type": role.BoundClaimsType, 309 "bound_claims": role.BoundClaims, 310 "claim_mappings": role.ClaimMappings, 311 "user_claim": role.UserClaim, 312 "groups_claim": role.GroupsClaim, 313 "allowed_redirect_uris": role.AllowedRedirectURIs, 314 "oidc_scopes": role.OIDCScopes, 315 "verbose_oidc_logging": role.VerboseOIDCLogging, 316 "max_age": int64(role.MaxAge.Seconds()), 317 } 318 319 role.PopulateTokenData(d) 320 321 if len(role.Policies) > 0 { 322 d["policies"] = d["token_policies"] 323 } 324 if len(role.BoundCIDRs) > 0 { 325 d["bound_cidrs"] = d["token_bound_cidrs"] 326 } 327 if role.TTL > 0 { 328 d["ttl"] = int64(role.TTL.Seconds()) 329 } 330 if role.MaxTTL > 0 { 331 d["max_ttl"] = int64(role.MaxTTL.Seconds()) 332 } 333 if role.Period > 0 { 334 d["period"] = int64(role.Period.Seconds()) 335 } 336 if role.NumUses > 0 { 337 d["num_uses"] = role.NumUses 338 } 339 340 return &logical.Response{ 341 Data: d, 342 }, nil 343} 344 345// pathRoleDelete removes the role from storage 346func (b *jwtAuthBackend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 347 roleName := data.Get("name").(string) 348 if roleName == "" { 349 return logical.ErrorResponse("role name required"), nil 350 } 351 352 // Delete the role itself 353 if err := req.Storage.Delete(ctx, rolePrefix+roleName); err != nil { 354 return nil, err 355 } 356 357 return nil, nil 358} 359 360// pathRoleCreateUpdate registers a new role with the backend or updates the options 361// of an existing role 362func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 363 roleName := data.Get("name").(string) 364 if roleName == "" { 365 return logical.ErrorResponse("missing role name"), nil 366 } 367 368 // Check if the role already exists 369 role, err := b.role(ctx, req.Storage, roleName) 370 if err != nil { 371 return nil, err 372 } 373 374 // Create a new entry object if this is a CreateOperation 375 if role == nil { 376 if req.Operation == logical.UpdateOperation { 377 return nil, errors.New("role entry not found during update operation") 378 } 379 role = new(jwtRole) 380 } 381 382 roleType := data.Get("role_type").(string) 383 if roleType == "" { 384 roleType = "oidc" 385 } 386 if roleType != "jwt" && roleType != "oidc" { 387 return logical.ErrorResponse("invalid 'role_type': %s", roleType), nil 388 } 389 role.RoleType = roleType 390 391 if err := role.ParseTokenFields(req, data); err != nil { 392 return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest 393 } 394 395 // Handle upgrade cases 396 { 397 if err := tokenutil.UpgradeValue(data, "policies", "token_policies", &role.Policies, &role.TokenPolicies); err != nil { 398 return logical.ErrorResponse(err.Error()), nil 399 } 400 401 if err := tokenutil.UpgradeValue(data, "bound_cidrs", "token_bound_cidrs", &role.BoundCIDRs, &role.TokenBoundCIDRs); err != nil { 402 return logical.ErrorResponse(err.Error()), nil 403 } 404 405 if err := tokenutil.UpgradeValue(data, "num_uses", "token_num_uses", &role.NumUses, &role.TokenNumUses); err != nil { 406 return logical.ErrorResponse(err.Error()), nil 407 } 408 409 if err := tokenutil.UpgradeValue(data, "ttl", "token_ttl", &role.TTL, &role.TokenTTL); err != nil { 410 return logical.ErrorResponse(err.Error()), nil 411 } 412 413 if err := tokenutil.UpgradeValue(data, "max_ttl", "token_max_ttl", &role.MaxTTL, &role.TokenMaxTTL); err != nil { 414 return logical.ErrorResponse(err.Error()), nil 415 } 416 417 if err := tokenutil.UpgradeValue(data, "period", "token_period", &role.Period, &role.TokenPeriod); err != nil { 418 return logical.ErrorResponse(err.Error()), nil 419 } 420 } 421 422 if role.TokenPeriod > b.System().MaxLeaseTTL() { 423 return logical.ErrorResponse(fmt.Sprintf("'period' of '%q' is greater than the backend's maximum lease TTL of '%q'", role.TokenPeriod.String(), b.System().MaxLeaseTTL().String())), nil 424 } 425 426 if tokenExpLeewayRaw, ok := data.GetOk("expiration_leeway"); ok { 427 role.ExpirationLeeway = time.Duration(tokenExpLeewayRaw.(int)) * time.Second 428 } 429 430 if tokenNotBeforeLeewayRaw, ok := data.GetOk("not_before_leeway"); ok { 431 role.NotBeforeLeeway = time.Duration(tokenNotBeforeLeewayRaw.(int)) * time.Second 432 } 433 434 if tokenClockSkewLeeway, ok := data.GetOk("clock_skew_leeway"); ok { 435 role.ClockSkewLeeway = time.Duration(tokenClockSkewLeeway.(int)) * time.Second 436 } 437 438 if boundAudiences, ok := data.GetOk("bound_audiences"); ok { 439 role.BoundAudiences = boundAudiences.([]string) 440 } 441 442 if boundSubject, ok := data.GetOk("bound_subject"); ok { 443 role.BoundSubject = boundSubject.(string) 444 } 445 446 if verboseOIDCLoggingRaw, ok := data.GetOk("verbose_oidc_logging"); ok { 447 role.VerboseOIDCLogging = verboseOIDCLoggingRaw.(bool) 448 } 449 450 if maxAgeRaw, ok := data.GetOk("max_age"); ok { 451 role.MaxAge = time.Duration(maxAgeRaw.(int)) * time.Second 452 } 453 454 boundClaimsType := data.Get("bound_claims_type").(string) 455 switch boundClaimsType { 456 case boundClaimsTypeString, boundClaimsTypeGlob: 457 role.BoundClaimsType = boundClaimsType 458 default: 459 return logical.ErrorResponse("invalid 'bound_claims_type': %s", boundClaimsType), nil 460 } 461 462 if boundClaimsRaw, ok := data.GetOk("bound_claims"); ok { 463 role.BoundClaims = boundClaimsRaw.(map[string]interface{}) 464 465 if boundClaimsType == boundClaimsTypeGlob { 466 // Check that the claims are all strings 467 for _, claimValues := range role.BoundClaims { 468 claimsValuesList, ok := normalizeList(claimValues) 469 470 if !ok { 471 return logical.ErrorResponse("claim is not a string or list: %v", claimValues), nil 472 } 473 474 for _, claimValue := range claimsValuesList { 475 if _, ok := claimValue.(string); !ok { 476 return logical.ErrorResponse("claim is not a string: %v", claimValue), nil 477 } 478 } 479 } 480 } 481 } 482 483 if claimMappingsRaw, ok := data.GetOk("claim_mappings"); ok { 484 claimMappings := claimMappingsRaw.(map[string]string) 485 486 // sanity check mappings for duplicates and collision with reserved names 487 targets := make(map[string]bool) 488 for _, metadataKey := range claimMappings { 489 if strutil.StrListContains(reservedMetadata, metadataKey) { 490 return logical.ErrorResponse("metadata key %q is reserved and may not be a mapping destination", metadataKey), nil 491 } 492 493 if targets[metadataKey] { 494 return logical.ErrorResponse("multiple keys are mapped to metadata key %q", metadataKey), nil 495 } 496 targets[metadataKey] = true 497 } 498 499 role.ClaimMappings = claimMappings 500 } 501 502 if userClaim, ok := data.GetOk("user_claim"); ok { 503 role.UserClaim = userClaim.(string) 504 } 505 if role.UserClaim == "" { 506 return logical.ErrorResponse("a user claim must be defined on the role"), nil 507 } 508 509 if groupsClaim, ok := data.GetOk("groups_claim"); ok { 510 role.GroupsClaim = groupsClaim.(string) 511 } 512 513 if oidcScopes, ok := data.GetOk("oidc_scopes"); ok { 514 role.OIDCScopes = oidcScopes.([]string) 515 } 516 517 if allowedRedirectURIs, ok := data.GetOk("allowed_redirect_uris"); ok { 518 role.AllowedRedirectURIs = allowedRedirectURIs.([]string) 519 } 520 521 if role.RoleType == "oidc" && len(role.AllowedRedirectURIs) == 0 { 522 return logical.ErrorResponse( 523 "'allowed_redirect_uris' must be set if 'role_type' is 'oidc' or unspecified."), nil 524 } 525 526 // OIDC verification will enforce that the audience match the configured client_id. 527 // For other methods, require at least one bound constraint. 528 if roleType != "oidc" { 529 if len(role.BoundAudiences) == 0 && 530 len(role.TokenBoundCIDRs) == 0 && 531 role.BoundSubject == "" && 532 len(role.BoundClaims) == 0 { 533 return logical.ErrorResponse("must have at least one bound constraint when creating/updating a role"), nil 534 } 535 } 536 537 // Check that the TTL value provided is less than the MaxTTL. 538 // Sanitizing the TTL and MaxTTL is not required now and can be performed 539 // at credential issue time. 540 if role.TokenMaxTTL > 0 && role.TokenTTL > role.TokenMaxTTL { 541 return logical.ErrorResponse("ttl should not be greater than max ttl"), nil 542 } 543 544 resp := &logical.Response{} 545 if role.TokenMaxTTL > b.System().MaxLeaseTTL() { 546 resp.AddWarning("token max ttl is greater than the system or backend mount's maximum TTL value; issued tokens' max TTL value will be truncated") 547 } 548 549 if role.VerboseOIDCLogging { 550 resp.AddWarning(`verbose_oidc_logging has been enabled for this role. ` + 551 `This is not recommended in production since sensitive information ` + 552 `may be present in OIDC responses.`) 553 } 554 555 // Store the entry. 556 entry, err := logical.StorageEntryJSON(rolePrefix+roleName, role) 557 if err != nil { 558 return nil, err 559 } 560 if err = req.Storage.Put(ctx, entry); err != nil { 561 return nil, err 562 } 563 564 return resp, nil 565} 566 567// roleStorageEntry stores all the options that are set on an role 568var roleHelp = map[string][2]string{ 569 "role-list": { 570 "Lists all the roles registered with the backend.", 571 "The list will contain the names of the roles.", 572 }, 573 "role": { 574 "Register an role with the backend.", 575 `A role is required to authenticate with this backend. The role binds 576 JWT token information with token policies and settings. 577 The bindings, token polices and token settings can all be configured 578 using this endpoint`, 579 }, 580} 581