package apptoken import ( "context" "fmt" "net/url" "strings" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) // config holds engine-wide settings: the JWT issuer URL, the signing algorithm // used for new keys, and how many retired keys stay published in the JWKS. type config struct { Issuer string `json:"issuer"` Algorithm string `json:"algorithm"` RetainedKeys int `json:"retained_keys"` } func defaultConfig() *config { return &config{Algorithm: algEdDSA, RetainedKeys: 1} } func pathConfig(b *appTokenBackend) *framework.Path { return &framework.Path{ Pattern: "config$", DisplayAttrs: &framework.DisplayAttributes{ OperationPrefix: "apptoken", OperationSuffix: "config", }, Fields: map[string]*framework.FieldSchema{ "issuer": { Type: framework.TypeString, Description: "External URL of this mount, used as the JWT `iss` claim and as the " + "base for the published jwks_uri (e.g. https://vault.example/v1/apptoken).", }, "algorithm": { Type: framework.TypeString, Description: "Signing algorithm for newly generated keys: EdDSA (default) or RS256.", Default: algEdDSA, }, "retained_keys": { Type: framework.TypeInt, Description: "Number of previous signing keys kept in the JWKS after a rotation, so tokens signed before the rotation still validate.", Default: 1, }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{Callback: b.pathConfigRead}, logical.CreateOperation: &framework.PathOperation{Callback: b.pathConfigWrite}, logical.UpdateOperation: &framework.PathOperation{Callback: b.pathConfigWrite}, }, ExistenceCheck: b.pathConfigExistenceCheck, HelpSynopsis: "Configure the issuer, signing algorithm and key retention.", HelpDescription: "Engine-wide settings shared by every role: the JWT issuer URL, the algorithm for new signing keys, and how many retired keys remain in the JWKS.", } } func (b *appTokenBackend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, _ *framework.FieldData) (bool, error) { cfg, err := b.getConfig(ctx, req.Storage) if err != nil { return false, err } return cfg != nil, nil } func (b *appTokenBackend) pathConfigRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { cfg, err := b.getConfig(ctx, req.Storage) if err != nil { return nil, err } if cfg == nil { return nil, nil } return &logical.Response{Data: map[string]interface{}{ "issuer": cfg.Issuer, "algorithm": cfg.Algorithm, "retained_keys": cfg.RetainedKeys, }}, nil } func (b *appTokenBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { cfg, err := b.getConfig(ctx, req.Storage) if err != nil { return nil, err } if cfg == nil { cfg = defaultConfig() } if v, ok := data.GetOk("issuer"); ok { issuer := strings.TrimRight(v.(string), "/") if issuer != "" { u, err := url.Parse(issuer) if err != nil || u.Scheme == "" || u.Host == "" { return logical.ErrorResponse("issuer must be an absolute URL"), nil } } cfg.Issuer = issuer } if v, ok := data.GetOk("algorithm"); ok { alg := v.(string) if alg != algEdDSA && alg != algRS256 { return logical.ErrorResponse("algorithm must be EdDSA or RS256"), nil } cfg.Algorithm = alg } if v, ok := data.GetOk("retained_keys"); ok { n := v.(int) if n < 0 { return logical.ErrorResponse("retained_keys must not be negative"), nil } cfg.RetainedKeys = n } if err := b.putConfig(ctx, req.Storage, cfg); err != nil { return nil, err } return nil, nil } func (b *appTokenBackend) getConfig(ctx context.Context, s logical.Storage) (*config, error) { entry, err := s.Get(ctx, configStoragePath) if err != nil { return nil, err } if entry == nil { return nil, nil } cfg := &config{} if err := entry.DecodeJSON(cfg); err != nil { return nil, err } if cfg.Algorithm == "" { cfg.Algorithm = algEdDSA } return cfg, nil } // getConfigOrDefault returns the stored config or a default one, so issuance // and key generation work before "config" is explicitly written. func (b *appTokenBackend) getConfigOrDefault(ctx context.Context, s logical.Storage) (*config, error) { cfg, err := b.getConfig(ctx, s) if err != nil { return nil, err } if cfg == nil { return defaultConfig(), nil } return cfg, nil } func (b *appTokenBackend) putConfig(ctx context.Context, s logical.Storage, cfg *config) error { entry, err := logical.StorageEntryJSON(configStoragePath, cfg) if err != nil { return fmt.Errorf("encoding config: %w", err) } return s.Put(ctx, entry) }