Skip to content
This repository has been archived by the owner on Feb 27, 2023. It is now read-only.

Commit

Permalink
Merge pull request #125 from shaxbee/nested-jwt
Browse files Browse the repository at this point in the history
Support creating and parsing nested JWTs
  • Loading branch information
csstaub authored Nov 17, 2016
2 parents 0549262 + ebda4b4 commit 296c7f1
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 19 deletions.
12 changes: 12 additions & 0 deletions crypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
type Encrypter interface {
Encrypt(plaintext []byte) (*JSONWebEncryption, error)
EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error)
Options() EncrypterOptions
}

// A generic content cipher
Expand Down Expand Up @@ -57,6 +58,7 @@ type keyDecrypter interface {
type genericEncrypter struct {
contentAlg ContentEncryption
compressionAlg CompressionAlgorithm
contentType ContentType
cipher contentCipher
recipients []recipientKeyInfo
keyGenerator keyGenerator
Expand All @@ -71,6 +73,7 @@ type recipientKeyInfo struct {
// EncrypterOptions represents options that can be set on new encrypters.
type EncrypterOptions struct {
Compression CompressionAlgorithm
ContentType ContentType
}

// Recipient represents an algorithm/key to encrypt messages to.
Expand All @@ -89,6 +92,7 @@ func NewEncrypter(enc ContentEncryption, rcpt Recipient, opts *EncrypterOptions)
}
if opts != nil {
encrypter.compressionAlg = opts.Compression
encrypter.contentType = opts.ContentType
}

if encrypter.cipher == nil {
Expand Down Expand Up @@ -256,6 +260,7 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWe

obj.protected = &rawHeader{
Enc: ctx.contentAlg,
Cty: string(ctx.contentType),
}
obj.recipients = make([]recipientInfo, len(ctx.recipients))

Expand Down Expand Up @@ -312,6 +317,13 @@ func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JSONWe
return obj, nil
}

func (ctx *genericEncrypter) Options() EncrypterOptions {
return EncrypterOptions{
Compression: ctx.compressionAlg,
ContentType: ctx.contentType,
}
}

// Decrypt and validate the object and return the plaintext. Note that this
// function does not support multi-recipient, if you desire multi-recipient
// decryption use DecryptMulti instead.
Expand Down
98 changes: 98 additions & 0 deletions jwt/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ type Builder interface {
CompactSerialize() (string, error)
}

// NestedBuilder is a utility for making Signed-Then-Encrypted JSON Web Tokens.
// Calls can be chained, and errors are accumulated until final call to
// CompactSerialize/FullSerialize.
type NestedBuilder interface {
// Claims encodes claims into JWE/JWS form. Multiple calls will merge claims
// into single JSON object.
Claims(i interface{}) NestedBuilder
// Token builds a NestedJSONWebToken from provided data.
Token() (*NestedJSONWebToken, error)
// FullSerialize serializes a token using the full serialization format.
FullSerialize() (string, error)
// CompactSerialize serializes a token using the compact serialization format.
CompactSerialize() (string, error)
}

type builder struct {
payload map[string]interface{}
err error
Expand All @@ -54,6 +69,12 @@ type encryptedBuilder struct {
enc jose.Encrypter
}

type nestedBuilder struct {
builder
sig jose.Signer
enc jose.Encrypter
}

// Signed creates builder for signed tokens.
func Signed(sig jose.Signer) Builder {
return &signedBuilder{
Expand All @@ -68,6 +89,22 @@ func Encrypted(enc jose.Encrypter) Builder {
}
}

// SignedAndEncrypted creates builder for signed-then-encrypted tokens.
// ErrInvalidContentType will be returned if encrypter doesn't have JWT content type.
func SignedAndEncrypted(sig jose.Signer, enc jose.Encrypter) NestedBuilder {
if enc.Options().ContentType != "JWT" {
return &nestedBuilder{
builder: builder{
err: ErrInvalidContentType,
},
}
}
return &nestedBuilder{
sig: sig,
enc: enc,
}
}

func (b builder) claims(i interface{}) builder {
if b.err != nil {
return b
Expand Down Expand Up @@ -225,3 +262,64 @@ func (b *encryptedBuilder) encrypt() (*jose.JSONWebEncryption, error) {

return b.enc.Encrypt(p)
}

func (b *nestedBuilder) Claims(i interface{}) NestedBuilder {
return &nestedBuilder{
builder: b.builder.claims(i),
sig: b.sig,
enc: b.enc,
}
}

func (b *nestedBuilder) Token() (*NestedJSONWebToken, error) {
enc, err := b.signAndEncrypt()
if err != nil {
return nil, err
}

return &NestedJSONWebToken{
enc: enc,
Headers: []jose.Header{enc.Header},
}, nil
}

func (b *nestedBuilder) CompactSerialize() (string, error) {
enc, err := b.signAndEncrypt()
if err != nil {
return "", err
}

return enc.CompactSerialize()
}

func (b *nestedBuilder) FullSerialize() (string, error) {
enc, err := b.signAndEncrypt()
if err != nil {
return "", err
}

return enc.FullSerialize(), nil
}

func (b *nestedBuilder) signAndEncrypt() (*jose.JSONWebEncryption, error) {
if b.err != nil {
return nil, b.err
}

p, err := json.Marshal(b.payload)
if err != nil {
return nil, err
}

sig, err := b.sig.Sign(p)
if err != nil {
return nil, err
}

p2, err := sig.CompactSerialize()
if err != nil {
return nil, err
}

return b.enc.Encrypt([]byte(p2))
}
55 changes: 55 additions & 0 deletions jwt/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,61 @@ func TestEncryptedFullSerializeAndToken(t *testing.T) {
require.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.")
}

func TestBuilderSignedAndEncrypted(t *testing.T) {
recipient := jose.Recipient{
Algorithm: jose.RSA1_5,
Key: testPrivRSAKey1.Public(),
}
encrypter, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, &jose.EncrypterOptions{
ContentType: "JWT",
})
require.NoError(t, err, "Error creating encrypter.")

jwt1, err := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"}).Token()
require.NoError(t, err, "Error marshaling signed-then-encrypted token.")
if nested, err := jwt1.Decrypt(testPrivRSAKey1); assert.NoError(t, err, "Error decrypting signed-then-encrypted token.") {
out := &testClaims{}
assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
assert.Equal(t, &testClaims{"foo"}, out)
}

b := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"})
tok1, err := b.CompactSerialize()
if assert.NoError(t, err) {
jwt, err := ParseSignedAndEncrypted(tok1)
if assert.NoError(t, err, "Error parsing signed-then-encrypted compact token.") {
if nested, err := jwt.Decrypt(testPrivRSAKey1); assert.NoError(t, err) {
out := &testClaims{}
assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
assert.Equal(t, &testClaims{"foo"}, out)
}
}
}

tok2, err := b.FullSerialize()
if assert.NoError(t, err) {
jwt, err := ParseSignedAndEncrypted(tok2)
if assert.NoError(t, err, "Error parsing signed-then-encrypted full token.") {
if nested, err := jwt.Decrypt(testPrivRSAKey1); assert.NoError(t, err) {
out := &testClaims{}
assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
assert.Equal(t, &testClaims{"foo"}, out)
}
}
}

b2 := SignedAndEncrypted(rsaSigner, encrypter).Claims(&invalidMarshalClaims{})
_, err = b2.CompactSerialize()
assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.")
_, err = b2.FullSerialize()
assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.")

encrypter2, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, nil)
require.NoError(t, err, "Error creating encrypter.")
_, err = SignedAndEncrypted(rsaSigner, encrypter2).CompactSerialize()
assert.EqualError(t, err, "square/go-jose/jwt: expected content type to be JWT (cty header)")
}

func TestBuilderHeadersSigner(t *testing.T) {
tests := []struct {
Keys []*rsa.PrivateKey
Expand Down
3 changes: 3 additions & 0 deletions jwt/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ var ErrNotValidYet = errors.New("square/go-jose/jwt: validation failed, token no

// ErrExpired indicates that token is used after expiry time indicated in exp claim.
var ErrExpired = errors.New("square/go-jose/jwt: validation failed, token is expired (exp)")

// ErrInvalidContentType indicated that token requires JWT cty header.
var ErrInvalidContentType = errors.New("square/go-jose/jwt: expected content type to be JWT (cty header)")
107 changes: 106 additions & 1 deletion jwt/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"strings"
"time"

"crypto/rsa"
"crypto/x509"
"encoding/pem"
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)
Expand Down Expand Up @@ -58,7 +61,28 @@ func ExampleParseEncrypted() {
panic(err)
}
fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
//Output: iss: issuer, sub: subject
// Output: iss: issuer, sub: subject
}

func ExampleParseSignedAndEncrypted() {
raw := `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIiwiY3R5IjoiSldUIn0..-keV-9YpsxotBEHw.yC9SHWgnkjykgJqXZGlzYC5Wg_EdWKO5TgfqeqsWWJYw7fX9zXQE3NtXmA3nAiUrYOr3H2s0AgTeAhTNbELLEHQu0blfRaPa_uKOAgFgmhJwbGe2iFLn9J0U72wk56318nI-pTLCV8FijoGpXvAxQlaKrPLKkl9yDQimPhb7UiDwLWYkJeoayciAXhR5f40E8ORGjCz8oawXRvjDaSjgRElUwy4kMGzvJy_difemEh4lfMSIwUNVEqJkEYaalRttSymMYuV6NvBVU0N0Jb6omdM4tW961OySB4KPWCWH9UJUX0XSEcqbW9WLxpg3ftx5R7xNiCnaVaCx_gJZfXJ9yFLqztIrKh2N05zHM0tddSOwCOnq7_1rJtaVz0nTXjSjf1RrVaxJya59p3K-e41QutiGFiJGzXG-L2OyLETIaVSU3ptvaCz4IxCF3GzeCvOgaICvXkpBY1-bv-fk1ilyjmcTDnLp2KivWIxcnoQmpN9xj06ZjagdG09AHUhS5WixADAg8mIdGcanNblALecnCWG-otjM9Kw.RZoaHtSgnzOin2od3D9tnA`
tok, err := jwt.ParseSignedAndEncrypted(raw)
if err != nil {
panic(err)
}

nested, err := tok.Decrypt(sharedEncryptionKey)
if err != nil {
panic(err)
}

out := jwt.Claims{}
if err := nested.Claims(&rsaPrivKey.PublicKey, &out); err != nil {
panic(err)
}

fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
// Output: iss: issuer, sub: subject
}

func ExampleClaims_Validate() {
Expand Down Expand Up @@ -146,6 +170,32 @@ func ExampleEncrypted() {
fmt.Println(raw)
}

func ExampleSignedAndEncrypted() {
enc, err := jose.NewEncrypter(
jose.A128GCM,
jose.Recipient{
Algorithm: jose.DIRECT,
Key: sharedEncryptionKey,
},
&jose.EncrypterOptions{
ContentType: "JWT",
})
if err != nil {
panic(err)
}

cl := jwt.Claims{
Subject: "subject",
Issuer: "issuer",
}
raw, err := jwt.SignedAndEncrypted(rsaSigner, enc).Claims(cl).CompactSerialize()
if err != nil {
panic(err)
}

fmt.Println(raw)
}

func ExampleSigned_multipleClaims() {
c := &jwt.Claims{
Subject: "subject",
Expand Down Expand Up @@ -198,3 +248,58 @@ func ExampleJSONWebToken_Claims_multiple() {
fmt.Printf("iss: %s, sub: %s, scopes: %s\n", out.Issuer, out.Subject, strings.Join(out2.Scopes, ","))
// Output: iss: issuer, sub: subject, scopes: foo,bar
}

func mustUnmarshalRSA(data string) *rsa.PrivateKey {
block, _ := pem.Decode([]byte(data))
if block == nil {
panic("failed to decode PEM data")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
panic("failed to parse RSA key: " + err.Error())
}
if key, ok := key.(*rsa.PrivateKey); ok {
return key
}
panic("key is not of type *rsa.PrivateKey")
}

func mustMakeSigner(alg jose.SignatureAlgorithm, k interface{}) jose.Signer {
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: k}, nil)
if err != nil {
panic("failed to create signer:" + err.Error())
}

return sig
}

var rsaPrivKey = mustUnmarshalRSA(`-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIHBvDHAr7jh8h
xaqBCl11fjI9YZtdC5b3HtXTXZW3c2dIOImNUjffT8POP6p5OpzivmC1om7iOyuZ
3nJjC9LT3zqqs3f2i5d4mImxEuqG6uWdryFfkp0uIv5VkjVO+iQWd6pDAPGP7r1Z
foXCleyCtmyNH4JSkJneNPOk/4BxO8vcvRnCMT/Gv81IT6H+OQ6OovWOuJr8RX9t
1wuCjC9ezZxeI9ONffhiO5FMrVh5H9LJTl3dPOVa4aEcOvgd45hBmvxAyXqf8daE
6Kl2O7vQ4uwgnSTVXYIIjCjbepuersApIMGx/XPSgiU1K3Xtah/TBvep+S3VlwPc
q/QH25S9AgMBAAECggEAe+y8XKYfPw4SxY1uPB+5JSwT3ON3nbWxtjSIYy9Pqp5z
Vcx9kuFZ7JevQSk4X38m7VzM8282kC/ono+d8yy9Uayq3k/qeOqV0X9Vti1qxEbw
ECkG1/MqGApfy4qSLOjINInDDV+mOWa2KJgsKgdCwuhKbVMYGB2ozG2qfYIlfvlY
vLcBEpGWmswJHNmkcjTtGFIyJgPbsI6ndkkOeQbqQKAaadXtG1xUzH+vIvqaUl/l
AkNf+p4qhPkHsoAWXf1qu9cYa2T8T+mEo79AwlgVC6awXQWNRTiyClDJC7cu6NBy
ZHXCLFMbalzWF9qeI2OPaFX2x3IBWrbyDxcJ4TSdQQKBgQD/Fp/uQonMBh1h4Vi4
HlxZdqSOArTitXValdLFGVJ23MngTGV/St4WH6eRp4ICfPyldsfcv6MZpNwNm1Rn
lB5Gtpqpby1dsrOSfvVbY7U3vpLnd8+hJ/lT5zCYt5Eor46N6iWRkYWzNe4PixiF
z1puGUvFCbZdeeACVrPLmW3JKQKBgQDI0y9WTf8ezKPbtap4UEE6yBf49ftohVGz
p4iD6Ng1uqePwKahwoVXKOc179CjGGtW/UUBORAoKRmxdHajHq6LJgsBxpaARz21
COPy99BUyp9ER5P8vYn63lC7Cpd/K7uyMjaz1DAzYBZIeVZHIw8O9wuGNJKjRFy9
SZyD3V0ddQKBgFMdohrWH2QVEfnUnT3Q1rJn0BJdm2bLTWOosbZ7G72TD0xAWEnz
sQ1wXv88n0YER6X6YADziEdQykq8s/HT91F/KkHO8e83zP8M0xFmGaQCOoelKEgQ
aFMIX3NDTM7+9OoUwwz9Z50PE3SJFAJ1n7eEEoYvNfabQXxBl+/dHEKRAoGAPEvU
EaiXacrtg8EWrssB2sFLGU/ZrTciIbuybFCT4gXp22pvXXAHEvVP/kzDqsRhLhwb
BNP6OuSkNziNikpjA5pngZ/7fgZly54gusmW/m5bxWdsUl0iOXVYbeAvPlqGH2me
LP4Pfs1hw17S/cbT9Z1NE31jbavP4HFikeD73SUCgYEArQfuudml6ei7XZ1Emjq8
jZiD+fX6e6BD/ISatVnuyZmGj9wPFsEhY2BpLiAMQHMDIvH9nlKzsFvjkTPB86qG
jCh3D67Os8eSBk5uRC6iW3Fc4DXvB5EFS0W9/15Sl+V5vXAcrNMpYS82OTSMG2Gt
b9Ym/nxaqyTu0PxajXkKm5Q=
-----END PRIVATE KEY-----`)

var rsaSigner = mustMakeSigner(jose.RS256, rsaPrivKey)
Loading

0 comments on commit 296c7f1

Please sign in to comment.