1
0
mirror of https://github.com/smallstep/cli.git synced 2025-04-19 10:42:15 +03:00

command/ca/token: support custom "user" claim

Add the `--set` and `--set-file` flags to the `step ca token` command,
allowing the user to set keys in the "user" claim in the resulting JWT.

Signed-off-by: Dan Fuhry <dan@fuhry.com>
This commit is contained in:
Dan Fuhry 2025-02-25 11:16:05 -05:00
parent f5a0bca360
commit 8abadfcd59
No known key found for this signature in database
GPG Key ID: DCD5C4E7C0B94A99
5 changed files with 79 additions and 1 deletions

View File

@ -34,7 +34,8 @@ func tokenCommand() cli.Command {
[**--sshpop-cert**=<file>] [**--sshpop-key**=<file>]
[**--cnf**=<fingerprint>] [**--cnf-file**=<file>]
[**--ssh**] [**--host**] [**--principal**=<name>] [**--k8ssa-token-path**=<file>]
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]`,
[**--ca-url**=<uri>] [**--root**=<file>] [**--context**=<name>]
[**--set**=<key=value>] [**--set-file**=<file>]`,
Description: `**step ca token** command generates a one-time token granting access to the
certificates authority.
@ -174,6 +175,18 @@ add the intermediate and the root in the provisioner configuration:
$ step ca token --kms yubikey:pin-value=123456 \
--x5c-cert yubikey:slot-id=82 --x5c-key yubikey:slot-id=82 \
internal.example.com
'''
Generate a token with custom data in the "user" claim. The example below can be
accessed in a template as **{{ .Token.user.field }}**, rendering to the string
"value".
This is distinct from **.Insecure.User**: any attributes set using this option
are added to a claim named "user" in the signed JWT produced by this command.
This data may therefore be considered trusted (insofar as the token itself is
trusted).
'''
$ step ca token --set field=value internal.example.com
'''`,
Flags: []cli.Flag{
provisionerKidFlag,
@ -244,6 +257,8 @@ be invalid for any other API request.`,
flags.CaURL,
flags.Root,
flags.Context,
flags.TemplateSet,
flags.TemplateSetFile,
},
}
}
@ -350,11 +365,29 @@ func tokenAction(ctx *cli.Context) error {
tokenOpts = append(tokenOpts, cautils.WithConfirmationFingerprint(cnf))
}
templateData, err := flags.GetTemplateData(ctx)
if err != nil {
return err
}
if templateData != nil {
tokenOpts = append(tokenOpts, cautils.WithCustomAttributes(templateData))
}
// --san and --type revoke are incompatible. Revocation tokens do not support SANs.
if typ == cautils.RevokeType && len(sans) > 0 {
return errs.IncompatibleFlagWithFlag(ctx, "san", "revoke")
}
// --offline doesn't support tokenOpts, so reject set/set-file
if offline {
if len(ctx.StringSlice("set")) > 0 {
return errs.IncompatibleFlagWithFlag(ctx, "offline", "set")
}
if ctx.String("set-file") != "" {
return errs.IncompatibleFlagWithFlag(ctx, "offline", "set-file")
}
}
// parse times or durations
notBefore, ok := flags.ParseTimeOrDuration(ctx.String("not-before"))
if !ok {

View File

@ -80,6 +80,25 @@ func WithStep(v interface{}) Options {
}
}
// WithUserData returns an Option function that merges the provided map with the
// existing user claim in the payload.
func WithUserData(v map[string]interface{}) Options {
return func(c *Claims) error {
if _, ok := c.ExtraClaims[UserClaim]; !ok {
c.Set(UserClaim, make(map[string]interface{}))
}
s := c.ExtraClaims[UserClaim]
sm, ok := s.(map[string]interface{})
if !ok {
return fmt.Errorf("%q claim is %T, not map[string]interface{}", UserClaim, s)
}
for k, val := range v {
sm[k] = val
}
return nil
}
}
// WithSSH returns an Options function that sets the step claim with the ssh
// property in the value.
func WithSSH(v interface{}) Options {

View File

@ -32,6 +32,9 @@ const SANSClaim = "sans"
// StepClaim is the property name for a JWT claim the stores the custom information in the certificate.
const StepClaim = "step"
// UserClaim is the property name for a JWT claim that stores user-provided custom information.
const UserClaim = "user"
// ConfirmationClaim is the property name for a JWT claim that stores a JSON
// object used as Proof-Of-Possession.
const ConfirmationClaim = "cnf"

View File

@ -43,6 +43,7 @@ type flowContext struct {
SSHPublicKey ssh.PublicKey
CertificateRequest *x509.CertificateRequest
ConfirmationFingerprint string
CustomAttributes map[string]interface{}
}
// sharedContext is used to share information between commands.
@ -88,6 +89,18 @@ func WithConfirmationFingerprint(fp string) Option {
})
}
// WithCustomAttributes adds custom attributes to be set in the "user" claim.
func WithCustomAttributes(v map[string]interface{}) Option {
return newFuncFlowOption(func(fo *flowContext) {
if fo.CustomAttributes == nil {
fo.CustomAttributes = make(map[string]interface{})
}
for k, val := range v {
fo.CustomAttributes[k] = val
}
})
}
// NewCertificateFlow initializes a cli flow to get a new certificate.
func NewCertificateFlow(ctx *cli.Context, opts ...Option) (*CertificateFlow, error) {
var err error

View File

@ -108,6 +108,11 @@ func (t *TokenGenerator) SignToken(sub string, sans []string, opts ...token.Opti
opts = append(opts, token.WithConfirmationFingerprint(sharedContext.ConfirmationFingerprint))
}
// Add custom user data, if set.
if sharedContext.CustomAttributes != nil {
opts = append(opts, token.WithUserData(sharedContext.CustomAttributes))
}
return t.Token(sub, opts...)
}
@ -126,6 +131,11 @@ func (t *TokenGenerator) SignSSHToken(sub, certType string, principals []string,
ValidBefore: notAfter,
})}, opts...)
// Add custom user data, if set.
if sharedContext.CustomAttributes != nil {
opts = append(opts, token.WithUserData(sharedContext.CustomAttributes))
}
return t.Token(sub, opts...)
}