Skip to content

Commit f166feb

Browse files
lestrratCopilot
andauthoredMar 28, 2025
Implement AWS ALB User Claims parser (#1328)
* [jws][jwt] Implement WithBase64Encoder * remove unused nolint * tweak for bazel * move test to openid this is easier to handle than doing it in jwt, because of how the dependencies work * revert jws.Signature API change, workaround it * Tweak comments * Update Changes * Update jws/options_gen.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update jws/options.yaml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 306ebc3 commit f166feb

14 files changed

+275
-10
lines changed
 

‎Changes

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ Changes
44
v3 has many incompatibilities with v2. To see the full list of differences between
55
v2 and v3, please read the Changes-v3.md file (https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes-v3.md)
66

7+
v3.0.0-beta2 UNRELEASED
8+
* [jwk] Fix a bug where `jwk.Set`'s `Keys()` method did not return the proper
9+
non-standard fields. (#1322)
10+
* [jws][jwt] Implement `WithBase64Encoder()` options to pass base64 encoders
11+
to use during signing/verifying signatures. This useful when the token
12+
provider generates JWTs that don't follow the specification and uses base64
13+
encoding other than raw url encoding (no padding), such as, apparently,
14+
AWS ALB. (#1324, #1328)
15+
716
v3.0.0-beta1 15 Mar 2025
817
* [jwt] Token validation no longer truncates time based fields by default.
918
To restore old behavior, you can either change the global settings by

‎internal/base64/base64.go

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ func getEncoder() Encoder {
3535
return encoder
3636
}
3737

38+
func DefaultEncoder() Encoder {
39+
return getEncoder()
40+
}
41+
3842
func SetDecoder(dec Decoder) {
3943
muDecoder.Lock()
4044
defer muDecoder.Unlock()

‎jws/interface.go

+12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
package jws
22

33
import (
4+
"github.com/lestrrat-go/jwx/v3/internal/base64"
45
"github.com/lestrrat-go/jwx/v3/jwa"
56
)
67

8+
// Base64Encoder is an interface that can be used when encoding JWS message
9+
// components to base64. This is useful when you want to use a non-standard
10+
// base64 encoder while generating or verifying signatures. By default JWS
11+
// uses raw url base64 encoding (without padding), but there are apparently
12+
// some cases where you may want to use a base64 encoders that uses padding.
13+
//
14+
// For example, apparently AWS ALB User Claims is provided in JWT format,
15+
// but it uses a base64 encoding with padding.
16+
type Base64Encoder = base64.Encoder
17+
718
type DecodeCtx interface {
819
CollectRaw() bool
920
}
@@ -55,6 +66,7 @@ type Message struct {
5566
}
5667

5768
type Signature struct {
69+
encoder Base64Encoder
5870
dc DecodeCtx
5971
headers Headers // Unprotected Headers
6072
protected Headers // Protected Headers

‎jws/jws.go

+15-3
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
171171
var detached bool
172172
var noneSignature *payloadSigner
173173
var validateKey bool
174+
var encoder Base64Encoder = base64.DefaultEncoder()
174175
for _, option := range options {
175176
//nolint:forcetypeassert
176177
switch option.Ident() {
@@ -209,6 +210,8 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
209210
payload = option.Value().([]byte)
210211
case identValidateKey{}:
211212
validateKey = option.Value().(bool)
213+
case identBase64Encoder{}:
214+
encoder = option.Value().(Base64Encoder)
212215
}
213216
}
214217

@@ -260,6 +263,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
260263
sig := &Signature{
261264
headers: signer.PublicHeader(),
262265
protected: protected,
266+
encoder: encoder,
263267
// cheat. FIXXXXXXMEEEEEE
264268
detached: detached,
265269
}
@@ -289,6 +293,11 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
289293
if detached {
290294
compactOpts = append(compactOpts, WithDetached(detached))
291295
}
296+
for _, option := range options {
297+
if copt, ok := option.(CompactOption); ok {
298+
compactOpts = append(compactOpts, copt)
299+
}
300+
}
292301
return Compact(&result, compactOpts...)
293302
default:
294303
return nil, signerr(`invalid serialization format`)
@@ -328,6 +337,7 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
328337
var keyProviders []KeyProvider
329338
var keyUsed interface{}
330339
var validateKey bool
340+
var encoder Base64Encoder = base64.DefaultEncoder()
331341

332342
ctx := context.Background()
333343

@@ -359,6 +369,8 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
359369
validateKey = option.Value().(bool)
360370
case identSerialization{}:
361371
parseOptions = append(parseOptions, option.(ParseOption))
372+
case identBase64Encoder{}:
373+
encoder = option.Value().(Base64Encoder)
362374
default:
363375
return nil, verifyerr(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`))
364376
}
@@ -385,7 +397,7 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
385397
// Pre-compute the base64 encoded version of payload
386398
var payload string
387399
if msg.b64 {
388-
payload = base64.EncodeToString(msg.payload)
400+
payload = encoder.EncodeToString(msg.payload)
389401
} else {
390402
payload = string(msg.payload)
391403
}
@@ -400,7 +412,7 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
400412
var encodedProtectedHeader string
401413
if rbp, ok := sig.protected.(interface{ rawBuffer() []byte }); ok {
402414
if raw := rbp.rawBuffer(); raw != nil {
403-
encodedProtectedHeader = base64.EncodeToString(raw)
415+
encodedProtectedHeader = encoder.EncodeToString(raw)
404416
}
405417
}
406418

@@ -410,7 +422,7 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
410422
return nil, verifyerr(`failed to marshal "protected" for signature #%d: %w`, i+1, err)
411423
}
412424

413-
encodedProtectedHeader = base64.EncodeToString(protected)
425+
encodedProtectedHeader = encoder.EncodeToString(protected)
414426
}
415427

416428
verifyBuf.WriteString(encodedProtectedHeader)

‎jws/message.go

+13-6
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,17 @@ func (s *Signature) Sign(payload []byte, signer Signer, key interface{}) ([]byte
127127
buf := pool.GetBytesBuffer()
128128
defer pool.ReleaseBytesBuffer(buf)
129129

130-
buf.WriteString(base64.EncodeToString(hdrbuf))
130+
encoder := s.encoder
131+
if encoder == nil {
132+
encoder = base64.DefaultEncoder()
133+
}
134+
buf.WriteString(encoder.EncodeToString(hdrbuf))
131135
buf.WriteByte('.')
132136

133137
var plen int
134138
b64 := getB64Value(hdrs)
135139
if b64 {
136-
encoded := base64.EncodeToString(payload)
140+
encoded := encoder.EncodeToString(payload)
137141
plen = len(encoded)
138142
buf.WriteString(encoded)
139143
} else {
@@ -158,7 +162,7 @@ func (s *Signature) Sign(payload []byte, signer Signer, key interface{}) ([]byte
158162
}
159163

160164
buf.WriteByte('.')
161-
buf.WriteString(base64.EncodeToString(signature))
165+
buf.WriteString(encoder.EncodeToString(signature))
162166
ret := make([]byte, buf.Len())
163167
copy(ret, buf.Bytes())
164168

@@ -466,11 +470,14 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
466470
}
467471

468472
var detached bool
473+
var encoder Base64Encoder = base64.DefaultEncoder()
469474
for _, option := range options {
470475
//nolint:forcetypeassert
471476
switch option.Ident() {
472477
case identDetached{}:
473478
detached = option.Value().(bool)
479+
case identBase64Encoder{}:
480+
encoder = option.Value().(Base64Encoder)
474481
}
475482
}
476483

@@ -486,12 +493,12 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
486493
buf := pool.GetBytesBuffer()
487494
defer pool.ReleaseBytesBuffer(buf)
488495

489-
buf.WriteString(base64.EncodeToString(hdrbuf))
496+
buf.WriteString(encoder.EncodeToString(hdrbuf))
490497
buf.WriteByte('.')
491498

492499
if !detached {
493500
if getB64Value(hdrs) {
494-
encoded := base64.EncodeToString(msg.payload)
501+
encoded := encoder.EncodeToString(msg.payload)
495502
buf.WriteString(encoded)
496503
} else {
497504
if bytes.Contains(msg.payload, []byte{'.'}) {
@@ -502,7 +509,7 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
502509
}
503510

504511
buf.WriteByte('.')
505-
buf.WriteString(base64.EncodeToString(s.signature))
512+
buf.WriteString(encoder.EncodeToString(s.signature))
506513
ret := make([]byte, buf.Len())
507514
copy(ret, buf.Bytes())
508515
return ret, nil

‎jws/options.yaml

+16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ interfaces:
2020
- parseOption
2121
comment: |
2222
SignVerifyOption describes options that can be passed to either `jws.Verify` or `jws.Sign`
23+
- name: SignVerifyCompactOption
24+
methods:
25+
- signOption
26+
- verifyOption
27+
- compactOption
28+
- parseOption
29+
comment: |
30+
SignVerifyCompactOption describes options that can be passed to either `jws.Verify`,
31+
`jws.Sign`, or `jws.Compact`
2332
- name: WithJSONSuboption
2433
concrete_type: withJSONSuboption
2534
comment: |
@@ -78,6 +87,13 @@ options:
7887
must be set to `nil`.
7988
8089
If you have to verify using this option, you should know exactly how and why this works.
90+
- ident: Base64Encoder
91+
interface: SignVerifyCompactOption
92+
argument_type: Base64Encoder
93+
comment: |
94+
WithBase64Encoder specifies the base64 encoder to be used while signing or
95+
verifying the JWS message. By default, the raw URL base64 encoding (no padding)
96+
is used.
8197
- ident: Message
8298
interface: VerifyOption
8399
argument_type: '*Message'

‎jws/options_gen.go

+34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎jws/options_gen_test.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎jwt/jwt.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ func parseBytes(data []byte, options ...ParseOption) (Token, error) {
201201

202202
//nolint:forcetypeassert
203203
switch o.Ident() {
204-
case identKey{}, identKeySet{}, identVerifyAuto{}, identKeyProvider{}:
204+
case identKey{}, identKeySet{}, identVerifyAuto{}, identKeyProvider{}, identBase64Encoder{}:
205205
verifyOpts = append(verifyOpts, o)
206206
case identToken{}:
207207
token, ok := o.Value().(Token)

0 commit comments

Comments
 (0)
Failed to load comments.