package apptoken import ( "context" "time" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) func pathCredentials(b *appTokenBackend) *framework.Path { return &framework.Path{ Pattern: "creds/" + framework.GenericNameRegex("name"), DisplayAttrs: &framework.DisplayAttributes{ OperationPrefix: "apptoken", OperationSuffix: "credentials", }, Fields: map[string]*framework.FieldSchema{ "name": { Type: framework.TypeLowerCaseString, Description: "Name of the role to mint a token for.", Required: true, }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{Callback: b.pathCredentialsRead}, logical.UpdateOperation: &framework.PathOperation{Callback: b.pathCredentialsRead}, }, HelpSynopsis: "Mint a signed JWT app token from a role.", HelpDescription: "Reading this path issues a short-lived JWT whose audience is the role's app. Services validate it offline against the engine's JWKS.", } } func (b *appTokenBackend) pathCredentialsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) role, err := b.getRole(ctx, req.Storage, name) if err != nil { return nil, err } if role == nil { return logical.ErrorResponse("role %q does not exist", name), nil } cfg, err := b.getConfigOrDefault(ctx, req.Storage) if err != nil { return nil, err } if cfg.Issuer == "" { return logical.ErrorResponse("issuer is not configured; set it via the config path"), nil } subject, ok := b.resolveSubject(req, role) if !ok { return logical.ErrorResponse("caller is not permitted to mint tokens from role %q", name), logical.ErrPermissionDenied } b.keyLock.Lock() ks, err := b.getOrCreateKeyset(ctx, req.Storage, cfg) b.keyLock.Unlock() if err != nil { return nil, err } ttl := b.resolveTTL(role) tok, err := signToken(ks.current(), cfg.Issuer, subject, role.audience(name), role.Claims, ttl, time.Now()) if err != nil { return nil, err } return &logical.Response{Data: map[string]interface{}{ "token": tok.Token, "token_type": "Bearer", "issuer": cfg.Issuer, "audience": tok.Audience, "subject": tok.Subject, "key_id": tok.KeyID, "algorithm": tok.Algorithm, "jti": tok.JTI, "issued_at": tok.IssuedAt.Format(time.RFC3339), "expires_at": tok.ExpiresAt.Format(time.RFC3339), "ttl_seconds": int64(ttl.Seconds()), }}, nil } // resolveSubject determines the JWT subject from the caller's Vault identity // and enforces the role's subject allowlist. It returns (subject, allowed). func (b *appTokenBackend) resolveSubject(req *logical.Request, role *appRole) (string, bool) { var candidates []string if req.EntityID != "" { candidates = append(candidates, req.EntityID) } if req.DisplayName != "" { candidates = append(candidates, req.DisplayName) } subject := "" if len(candidates) > 0 { subject = candidates[0] } if len(role.AllowedSubjects) == 0 { return subject, true } allowed := make(map[string]bool, len(role.AllowedSubjects)) for _, s := range role.AllowedSubjects { allowed[s] = true } for _, c := range candidates { if allowed[c] { return c, true } } return subject, false } // resolveTTL clamps the role TTL against the role max and the mount/system // lease limits. func (b *appTokenBackend) resolveTTL(role *appRole) time.Duration { sysMax := b.System().MaxLeaseTTL() maxTTL := role.MaxTTL if maxTTL <= 0 || maxTTL > sysMax { maxTTL = sysMax } ttl := role.TTL if ttl <= 0 { ttl = b.System().DefaultLeaseTTL() } if ttl > maxTTL { ttl = maxTTL } return ttl }