package apptoken import ( "encoding/base64" "fmt" "time" jose "github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4/jwt" "github.com/hashicorp/go-uuid" ) func base64URL(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } // issuedToken is the result of minting a token: the compact JWT plus the // claims that let a caller reason about it without decoding. type issuedToken struct { Token string KeyID string Algorithm string Subject string Audience string IssuedAt time.Time ExpiresAt time.Time JTI string } // signToken builds and signs a JWT for the given role using the current // signing key. now/ttl are supplied by the caller so tests are deterministic. func signToken(key *signingKey, issuer, subject, audience string, extra map[string]string, ttl time.Duration, now time.Time) (*issuedToken, error) { signer, err := key.signer() if err != nil { return nil, err } jti, err := uuid.GenerateUUID() if err != nil { return nil, fmt.Errorf("generating jti: %w", err) } joseSigner, err := jose.NewSigner( jose.SigningKey{Algorithm: key.joseAlgorithm(), Key: signer}, (&jose.SignerOptions{}).WithType("JWT").WithHeader("kid", key.KeyID), ) if err != nil { return nil, fmt.Errorf("building signer: %w", err) } expiry := now.Add(ttl) registered := jwt.Claims{ Issuer: issuer, Subject: subject, Audience: jwt.Audience{audience}, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(expiry), ID: jti, } builder := jwt.Signed(joseSigner).Claims(registered) if len(extra) > 0 { claims := make(map[string]interface{}, len(extra)) for k, v := range extra { claims[k] = v } builder = builder.Claims(claims) } token, err := builder.Serialize() if err != nil { return nil, fmt.Errorf("serializing token: %w", err) } return &issuedToken{ Token: token, KeyID: key.KeyID, Algorithm: key.Algorithm, Subject: subject, Audience: audience, IssuedAt: now, ExpiresAt: expiry, JTI: jti, }, nil }