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

Commit 296c7f1

Browse files
authored
Merge pull request #125 from shaxbee/nested-jwt
Support creating and parsing nested JWTs
2 parents 0549262 + ebda4b4 commit 296c7f1

File tree

8 files changed

+365
-19
lines changed

8 files changed

+365
-19
lines changed

crypter.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
type Encrypter interface {
2929
Encrypt(plaintext []byte) (*JSONWebEncryption, error)
3030
EncryptWithAuthData(plaintext []byte, aad []byte) (*JSONWebEncryption, error)
31+
Options() EncrypterOptions
3132
}
3233

3334
// A generic content cipher
@@ -57,6 +58,7 @@ type keyDecrypter interface {
5758
type genericEncrypter struct {
5859
contentAlg ContentEncryption
5960
compressionAlg CompressionAlgorithm
61+
contentType ContentType
6062
cipher contentCipher
6163
recipients []recipientKeyInfo
6264
keyGenerator keyGenerator
@@ -71,6 +73,7 @@ type recipientKeyInfo struct {
7173
// EncrypterOptions represents options that can be set on new encrypters.
7274
type EncrypterOptions struct {
7375
Compression CompressionAlgorithm
76+
ContentType ContentType
7477
}
7578

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

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

257261
obj.protected = &rawHeader{
258262
Enc: ctx.contentAlg,
263+
Cty: string(ctx.contentType),
259264
}
260265
obj.recipients = make([]recipientInfo, len(ctx.recipients))
261266

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

320+
func (ctx *genericEncrypter) Options() EncrypterOptions {
321+
return EncrypterOptions{
322+
Compression: ctx.compressionAlg,
323+
ContentType: ctx.contentType,
324+
}
325+
}
326+
315327
// Decrypt and validate the object and return the plaintext. Note that this
316328
// function does not support multi-recipient, if you desire multi-recipient
317329
// decryption use DecryptMulti instead.

jwt/builder.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ type Builder interface {
3939
CompactSerialize() (string, error)
4040
}
4141

42+
// NestedBuilder is a utility for making Signed-Then-Encrypted JSON Web Tokens.
43+
// Calls can be chained, and errors are accumulated until final call to
44+
// CompactSerialize/FullSerialize.
45+
type NestedBuilder interface {
46+
// Claims encodes claims into JWE/JWS form. Multiple calls will merge claims
47+
// into single JSON object.
48+
Claims(i interface{}) NestedBuilder
49+
// Token builds a NestedJSONWebToken from provided data.
50+
Token() (*NestedJSONWebToken, error)
51+
// FullSerialize serializes a token using the full serialization format.
52+
FullSerialize() (string, error)
53+
// CompactSerialize serializes a token using the compact serialization format.
54+
CompactSerialize() (string, error)
55+
}
56+
4257
type builder struct {
4358
payload map[string]interface{}
4459
err error
@@ -54,6 +69,12 @@ type encryptedBuilder struct {
5469
enc jose.Encrypter
5570
}
5671

72+
type nestedBuilder struct {
73+
builder
74+
sig jose.Signer
75+
enc jose.Encrypter
76+
}
77+
5778
// Signed creates builder for signed tokens.
5879
func Signed(sig jose.Signer) Builder {
5980
return &signedBuilder{
@@ -68,6 +89,22 @@ func Encrypted(enc jose.Encrypter) Builder {
6889
}
6990
}
7091

92+
// SignedAndEncrypted creates builder for signed-then-encrypted tokens.
93+
// ErrInvalidContentType will be returned if encrypter doesn't have JWT content type.
94+
func SignedAndEncrypted(sig jose.Signer, enc jose.Encrypter) NestedBuilder {
95+
if enc.Options().ContentType != "JWT" {
96+
return &nestedBuilder{
97+
builder: builder{
98+
err: ErrInvalidContentType,
99+
},
100+
}
101+
}
102+
return &nestedBuilder{
103+
sig: sig,
104+
enc: enc,
105+
}
106+
}
107+
71108
func (b builder) claims(i interface{}) builder {
72109
if b.err != nil {
73110
return b
@@ -225,3 +262,64 @@ func (b *encryptedBuilder) encrypt() (*jose.JSONWebEncryption, error) {
225262

226263
return b.enc.Encrypt(p)
227264
}
265+
266+
func (b *nestedBuilder) Claims(i interface{}) NestedBuilder {
267+
return &nestedBuilder{
268+
builder: b.builder.claims(i),
269+
sig: b.sig,
270+
enc: b.enc,
271+
}
272+
}
273+
274+
func (b *nestedBuilder) Token() (*NestedJSONWebToken, error) {
275+
enc, err := b.signAndEncrypt()
276+
if err != nil {
277+
return nil, err
278+
}
279+
280+
return &NestedJSONWebToken{
281+
enc: enc,
282+
Headers: []jose.Header{enc.Header},
283+
}, nil
284+
}
285+
286+
func (b *nestedBuilder) CompactSerialize() (string, error) {
287+
enc, err := b.signAndEncrypt()
288+
if err != nil {
289+
return "", err
290+
}
291+
292+
return enc.CompactSerialize()
293+
}
294+
295+
func (b *nestedBuilder) FullSerialize() (string, error) {
296+
enc, err := b.signAndEncrypt()
297+
if err != nil {
298+
return "", err
299+
}
300+
301+
return enc.FullSerialize(), nil
302+
}
303+
304+
func (b *nestedBuilder) signAndEncrypt() (*jose.JSONWebEncryption, error) {
305+
if b.err != nil {
306+
return nil, b.err
307+
}
308+
309+
p, err := json.Marshal(b.payload)
310+
if err != nil {
311+
return nil, err
312+
}
313+
314+
sig, err := b.sig.Sign(p)
315+
if err != nil {
316+
return nil, err
317+
}
318+
319+
p2, err := sig.CompactSerialize()
320+
if err != nil {
321+
return nil, err
322+
}
323+
324+
return b.enc.Encrypt([]byte(p2))
325+
}

jwt/builder_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,61 @@ func TestEncryptedFullSerializeAndToken(t *testing.T) {
179179
require.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.")
180180
}
181181

182+
func TestBuilderSignedAndEncrypted(t *testing.T) {
183+
recipient := jose.Recipient{
184+
Algorithm: jose.RSA1_5,
185+
Key: testPrivRSAKey1.Public(),
186+
}
187+
encrypter, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, &jose.EncrypterOptions{
188+
ContentType: "JWT",
189+
})
190+
require.NoError(t, err, "Error creating encrypter.")
191+
192+
jwt1, err := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"}).Token()
193+
require.NoError(t, err, "Error marshaling signed-then-encrypted token.")
194+
if nested, err := jwt1.Decrypt(testPrivRSAKey1); assert.NoError(t, err, "Error decrypting signed-then-encrypted token.") {
195+
out := &testClaims{}
196+
assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
197+
assert.Equal(t, &testClaims{"foo"}, out)
198+
}
199+
200+
b := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"})
201+
tok1, err := b.CompactSerialize()
202+
if assert.NoError(t, err) {
203+
jwt, err := ParseSignedAndEncrypted(tok1)
204+
if assert.NoError(t, err, "Error parsing signed-then-encrypted compact token.") {
205+
if nested, err := jwt.Decrypt(testPrivRSAKey1); assert.NoError(t, err) {
206+
out := &testClaims{}
207+
assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
208+
assert.Equal(t, &testClaims{"foo"}, out)
209+
}
210+
}
211+
}
212+
213+
tok2, err := b.FullSerialize()
214+
if assert.NoError(t, err) {
215+
jwt, err := ParseSignedAndEncrypted(tok2)
216+
if assert.NoError(t, err, "Error parsing signed-then-encrypted full token.") {
217+
if nested, err := jwt.Decrypt(testPrivRSAKey1); assert.NoError(t, err) {
218+
out := &testClaims{}
219+
assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
220+
assert.Equal(t, &testClaims{"foo"}, out)
221+
}
222+
}
223+
}
224+
225+
b2 := SignedAndEncrypted(rsaSigner, encrypter).Claims(&invalidMarshalClaims{})
226+
_, err = b2.CompactSerialize()
227+
assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.")
228+
_, err = b2.FullSerialize()
229+
assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: Failed marshaling invalid claims.")
230+
231+
encrypter2, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, nil)
232+
require.NoError(t, err, "Error creating encrypter.")
233+
_, err = SignedAndEncrypted(rsaSigner, encrypter2).CompactSerialize()
234+
assert.EqualError(t, err, "square/go-jose/jwt: expected content type to be JWT (cty header)")
235+
}
236+
182237
func TestBuilderHeadersSigner(t *testing.T) {
183238
tests := []struct {
184239
Keys []*rsa.PrivateKey

jwt/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,6 @@ var ErrNotValidYet = errors.New("square/go-jose/jwt: validation failed, token no
4545

4646
// ErrExpired indicates that token is used after expiry time indicated in exp claim.
4747
var ErrExpired = errors.New("square/go-jose/jwt: validation failed, token is expired (exp)")
48+
49+
// ErrInvalidContentType indicated that token requires JWT cty header.
50+
var ErrInvalidContentType = errors.New("square/go-jose/jwt: expected content type to be JWT (cty header)")

jwt/example_test.go

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
"strings"
2323
"time"
2424

25+
"crypto/rsa"
26+
"crypto/x509"
27+
"encoding/pem"
2528
"gopkg.in/square/go-jose.v2"
2629
"gopkg.in/square/go-jose.v2/jwt"
2730
)
@@ -58,7 +61,28 @@ func ExampleParseEncrypted() {
5861
panic(err)
5962
}
6063
fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
61-
//Output: iss: issuer, sub: subject
64+
// Output: iss: issuer, sub: subject
65+
}
66+
67+
func ExampleParseSignedAndEncrypted() {
68+
raw := `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIiwiY3R5IjoiSldUIn0..-keV-9YpsxotBEHw.yC9SHWgnkjykgJqXZGlzYC5Wg_EdWKO5TgfqeqsWWJYw7fX9zXQE3NtXmA3nAiUrYOr3H2s0AgTeAhTNbELLEHQu0blfRaPa_uKOAgFgmhJwbGe2iFLn9J0U72wk56318nI-pTLCV8FijoGpXvAxQlaKrPLKkl9yDQimPhb7UiDwLWYkJeoayciAXhR5f40E8ORGjCz8oawXRvjDaSjgRElUwy4kMGzvJy_difemEh4lfMSIwUNVEqJkEYaalRttSymMYuV6NvBVU0N0Jb6omdM4tW961OySB4KPWCWH9UJUX0XSEcqbW9WLxpg3ftx5R7xNiCnaVaCx_gJZfXJ9yFLqztIrKh2N05zHM0tddSOwCOnq7_1rJtaVz0nTXjSjf1RrVaxJya59p3K-e41QutiGFiJGzXG-L2OyLETIaVSU3ptvaCz4IxCF3GzeCvOgaICvXkpBY1-bv-fk1ilyjmcTDnLp2KivWIxcnoQmpN9xj06ZjagdG09AHUhS5WixADAg8mIdGcanNblALecnCWG-otjM9Kw.RZoaHtSgnzOin2od3D9tnA`
69+
tok, err := jwt.ParseSignedAndEncrypted(raw)
70+
if err != nil {
71+
panic(err)
72+
}
73+
74+
nested, err := tok.Decrypt(sharedEncryptionKey)
75+
if err != nil {
76+
panic(err)
77+
}
78+
79+
out := jwt.Claims{}
80+
if err := nested.Claims(&rsaPrivKey.PublicKey, &out); err != nil {
81+
panic(err)
82+
}
83+
84+
fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
85+
// Output: iss: issuer, sub: subject
6286
}
6387

6488
func ExampleClaims_Validate() {
@@ -146,6 +170,32 @@ func ExampleEncrypted() {
146170
fmt.Println(raw)
147171
}
148172

173+
func ExampleSignedAndEncrypted() {
174+
enc, err := jose.NewEncrypter(
175+
jose.A128GCM,
176+
jose.Recipient{
177+
Algorithm: jose.DIRECT,
178+
Key: sharedEncryptionKey,
179+
},
180+
&jose.EncrypterOptions{
181+
ContentType: "JWT",
182+
})
183+
if err != nil {
184+
panic(err)
185+
}
186+
187+
cl := jwt.Claims{
188+
Subject: "subject",
189+
Issuer: "issuer",
190+
}
191+
raw, err := jwt.SignedAndEncrypted(rsaSigner, enc).Claims(cl).CompactSerialize()
192+
if err != nil {
193+
panic(err)
194+
}
195+
196+
fmt.Println(raw)
197+
}
198+
149199
func ExampleSigned_multipleClaims() {
150200
c := &jwt.Claims{
151201
Subject: "subject",
@@ -198,3 +248,58 @@ func ExampleJSONWebToken_Claims_multiple() {
198248
fmt.Printf("iss: %s, sub: %s, scopes: %s\n", out.Issuer, out.Subject, strings.Join(out2.Scopes, ","))
199249
// Output: iss: issuer, sub: subject, scopes: foo,bar
200250
}
251+
252+
func mustUnmarshalRSA(data string) *rsa.PrivateKey {
253+
block, _ := pem.Decode([]byte(data))
254+
if block == nil {
255+
panic("failed to decode PEM data")
256+
}
257+
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
258+
if err != nil {
259+
panic("failed to parse RSA key: " + err.Error())
260+
}
261+
if key, ok := key.(*rsa.PrivateKey); ok {
262+
return key
263+
}
264+
panic("key is not of type *rsa.PrivateKey")
265+
}
266+
267+
func mustMakeSigner(alg jose.SignatureAlgorithm, k interface{}) jose.Signer {
268+
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: k}, nil)
269+
if err != nil {
270+
panic("failed to create signer:" + err.Error())
271+
}
272+
273+
return sig
274+
}
275+
276+
var rsaPrivKey = mustUnmarshalRSA(`-----BEGIN PRIVATE KEY-----
277+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDIHBvDHAr7jh8h
278+
xaqBCl11fjI9YZtdC5b3HtXTXZW3c2dIOImNUjffT8POP6p5OpzivmC1om7iOyuZ
279+
3nJjC9LT3zqqs3f2i5d4mImxEuqG6uWdryFfkp0uIv5VkjVO+iQWd6pDAPGP7r1Z
280+
foXCleyCtmyNH4JSkJneNPOk/4BxO8vcvRnCMT/Gv81IT6H+OQ6OovWOuJr8RX9t
281+
1wuCjC9ezZxeI9ONffhiO5FMrVh5H9LJTl3dPOVa4aEcOvgd45hBmvxAyXqf8daE
282+
6Kl2O7vQ4uwgnSTVXYIIjCjbepuersApIMGx/XPSgiU1K3Xtah/TBvep+S3VlwPc
283+
q/QH25S9AgMBAAECggEAe+y8XKYfPw4SxY1uPB+5JSwT3ON3nbWxtjSIYy9Pqp5z
284+
Vcx9kuFZ7JevQSk4X38m7VzM8282kC/ono+d8yy9Uayq3k/qeOqV0X9Vti1qxEbw
285+
ECkG1/MqGApfy4qSLOjINInDDV+mOWa2KJgsKgdCwuhKbVMYGB2ozG2qfYIlfvlY
286+
vLcBEpGWmswJHNmkcjTtGFIyJgPbsI6ndkkOeQbqQKAaadXtG1xUzH+vIvqaUl/l
287+
AkNf+p4qhPkHsoAWXf1qu9cYa2T8T+mEo79AwlgVC6awXQWNRTiyClDJC7cu6NBy
288+
ZHXCLFMbalzWF9qeI2OPaFX2x3IBWrbyDxcJ4TSdQQKBgQD/Fp/uQonMBh1h4Vi4
289+
HlxZdqSOArTitXValdLFGVJ23MngTGV/St4WH6eRp4ICfPyldsfcv6MZpNwNm1Rn
290+
lB5Gtpqpby1dsrOSfvVbY7U3vpLnd8+hJ/lT5zCYt5Eor46N6iWRkYWzNe4PixiF
291+
z1puGUvFCbZdeeACVrPLmW3JKQKBgQDI0y9WTf8ezKPbtap4UEE6yBf49ftohVGz
292+
p4iD6Ng1uqePwKahwoVXKOc179CjGGtW/UUBORAoKRmxdHajHq6LJgsBxpaARz21
293+
COPy99BUyp9ER5P8vYn63lC7Cpd/K7uyMjaz1DAzYBZIeVZHIw8O9wuGNJKjRFy9
294+
SZyD3V0ddQKBgFMdohrWH2QVEfnUnT3Q1rJn0BJdm2bLTWOosbZ7G72TD0xAWEnz
295+
sQ1wXv88n0YER6X6YADziEdQykq8s/HT91F/KkHO8e83zP8M0xFmGaQCOoelKEgQ
296+
aFMIX3NDTM7+9OoUwwz9Z50PE3SJFAJ1n7eEEoYvNfabQXxBl+/dHEKRAoGAPEvU
297+
EaiXacrtg8EWrssB2sFLGU/ZrTciIbuybFCT4gXp22pvXXAHEvVP/kzDqsRhLhwb
298+
BNP6OuSkNziNikpjA5pngZ/7fgZly54gusmW/m5bxWdsUl0iOXVYbeAvPlqGH2me
299+
LP4Pfs1hw17S/cbT9Z1NE31jbavP4HFikeD73SUCgYEArQfuudml6ei7XZ1Emjq8
300+
jZiD+fX6e6BD/ISatVnuyZmGj9wPFsEhY2BpLiAMQHMDIvH9nlKzsFvjkTPB86qG
301+
jCh3D67Os8eSBk5uRC6iW3Fc4DXvB5EFS0W9/15Sl+V5vXAcrNMpYS82OTSMG2Gt
302+
b9Ym/nxaqyTu0PxajXkKm5Q=
303+
-----END PRIVATE KEY-----`)
304+
305+
var rsaSigner = mustMakeSigner(jose.RS256, rsaPrivKey)

0 commit comments

Comments
 (0)