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