Skip to content

Commit 676662f

Browse files
author
Victor Mignot
committed
feat: add support for JWT from AWS Cognito by allowing bearer_jwt_allowed_auds=["*"]
Signed-off-by: Victor Mignot <victor.mignot-ext@randstad.fr>
1 parent ba3a490 commit 676662f

2 files changed

Lines changed: 111 additions & 10 deletions

File tree

main.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -653,9 +653,10 @@ func verifyIDTokenCommon(idToken *oidc.IDToken) error {
653653
return errors.New("token does not contain sub claim")
654654
}
655655

656-
// Currently limit implementation to single-audience tokens
657-
if len(idToken.Audience) != 1 {
658-
return errors.New("token does not contain exactly one audience")
656+
// Some providers (like AWS Cognito) omit the audience claim in M2M access tokens.
657+
// We allow 0 or 1 audience, rejecting only multiple audiences for now.
658+
if len(idToken.Audience) > 1 {
659+
return errors.New("token contains multiple audiences, which is currently not supported")
659660
}
660661

661662
if idToken.IssuedAt.IsZero() {
@@ -1033,25 +1034,30 @@ func authBearerToken(kong Kong, conf Config, oidcProvider *OIDCProvider) (bool,
10331034
return false, nil
10341035
}
10351036

1037+
// Extract claims early to access the client_id, as it might be used as a fallback for audience validation
1038+
var idTokenClaims map[string]any
1039+
if err := idToken.Claims(&idTokenClaims); err != nil {
1040+
return false, fmt.Errorf("extracting claims from token failed: %w", err)
1041+
}
1042+
1043+
clientID, _ := idTokenClaims["client_id"].(string)
10361044
foundAllowedAud := false
10371045

10381046
for _, allowedAud := range conf.BearerJWTAllowedAuds {
1039-
if slices.Contains(idToken.Audience, allowedAud) {
1047+
// Allow if wildcard "*", or if audience matches, or if client_id matches (useful for Cognito M2M tokens)
1048+
if allowedAud == "*" || slices.Contains(idToken.Audience, allowedAud) || (clientID != "" && allowedAud == clientID) {
10401049
foundAllowedAud = true
1050+
break
10411051
}
10421052
}
10431053

10441054
if !foundAllowedAud {
1045-
kong.LogWarn("Bearer JWT token did not contain any of the allowed audiences")
1055+
kong.LogWarn("Bearer JWT token did not contain any of the allowed audiences or client_id")
10461056

10471057
return false, nil
10481058
}
10491059

1050-
var idTokenClaims map[string]any
1051-
if err := idToken.Claims(&idTokenClaims); err != nil {
1052-
return false, fmt.Errorf("extracting claims from token failed: %w", err)
1053-
}
1054-
1060+
// Pass the already extracted claims to setServiceData
10551061
err = setServiceData(idTokenClaims, nil, conf, kong)
10561062
if err != nil {
10571063
return false, fmt.Errorf("error setting service data: %w", err)

main_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,3 +805,98 @@ func generateTestTLSArtifacts(t *testing.T, serverIP string) (caPEM, serverCertP
805805

806806
return caPEM, serverCertPEM, serverKeyPEM
807807
}
808+
809+
func TestBearerJWTWildcardAudience(t *testing.T) {
810+
mockOidcServer, _ := mockoidc.Run()
811+
defer mockOidcServer.Shutdown() //nolint:errcheck
812+
813+
cfg := mockOidcServer.Config()
814+
815+
pluginConfig, ok := New().(*Config)
816+
assert.True(t, ok)
817+
818+
pluginConfig.Issuer = cfg.Issuer
819+
pluginConfig.ClientID = cfg.ClientID
820+
pluginConfig.ClientSecret = cfg.ClientSecret
821+
pluginConfig.RedirectURI = "http://localhost/cb"
822+
pluginConfig.Scopes = []string{"openid", "profile", "email"}
823+
// Explicitly configure the wildcard audience
824+
pluginConfig.BearerJWTAllowedAuds = []string{"*"}
825+
pluginConfig.ConsumerName = "oidcuser"
826+
pluginConfig.HeadersFromClaims = map[string]string{
827+
"X-Oidc-Email": "email",
828+
}
829+
830+
user := &mockoidc.MockUser{
831+
Subject: "1234567890",
832+
Email: "wildcard.user@example.com",
833+
EmailVerified: true,
834+
}
835+
mockOidcServer.QueueUser(user)
836+
837+
provider, err := oidc.NewProvider(t.Context(), cfg.Issuer)
838+
require.NoError(t, err)
839+
840+
oauth2Config := oauth2.Config{
841+
ClientID: cfg.ClientID,
842+
ClientSecret: cfg.ClientSecret,
843+
RedirectURL: "http://localhost/cb",
844+
Endpoint: provider.Endpoint(),
845+
Scopes: pluginConfig.Scopes,
846+
}
847+
848+
state := "state5678"
849+
pkceVerifier := "pkce5678"
850+
851+
authCodeURL := oauth2Config.AuthCodeURL(state,
852+
oauth2.S256ChallengeOption(pkceVerifier))
853+
854+
httpClient := &http.Client{
855+
CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse },
856+
}
857+
resp, err := httpClient.Get(authCodeURL) //nolint:noctx
858+
require.NoError(t, err)
859+
860+
defer resp.Body.Close()
861+
862+
cbLocationHeader := resp.Header.Get("Location")
863+
parsedCBLoc, err := url.Parse(cbLocationHeader)
864+
require.NoError(t, err)
865+
866+
code := parsedCBLoc.Query().Get("code")
867+
868+
token, err := oauth2Config.Exchange(t.Context(), code,
869+
oauth2.VerifierOption(pkceVerifier))
870+
require.NoError(t, err)
871+
872+
rawIDToken, okToken := token.Extra("id_token").(string)
873+
if !okToken {
874+
t.Fatal("no id_token in token response")
875+
}
876+
877+
mockKong := NewMockKong(t)
878+
879+
ignoreLogCalls(mockKong)
880+
// Simulate a request with the Authorization header containing the JWT token
881+
mockKong.EXPECT().RequestGetHeader("authorization").Return("Bearer "+rawIDToken, nil)
882+
mockKong.EXPECT().CtxSetShared("authenticated_groups", []any{}).Return(nil)
883+
mockKong.EXPECT().ServiceRequestSetHeader("X-Oidc-Email", "wildcard.user@example.com").Return(nil)
884+
885+
consumer := entities.Consumer{
886+
Id: "ffe30af5-d167-519a-8bdc-2fa89a3aa280",
887+
Username: "oidcuser",
888+
}
889+
mockKong.EXPECT().ClientLoadConsumer("oidcuser", true).Return(consumer, nil)
890+
mockKong.EXPECT().ClientAuthenticate(&consumer, &client.AuthenticatedCredential{
891+
Id: "1234567890",
892+
ConsumerId: consumer.Id,
893+
}).Return(nil)
894+
mockKong.EXPECT().ServiceRequestSetHeader("X-Consumer-Id", consumer.Id).Return(nil)
895+
mockKong.EXPECT().ServiceRequestSetHeader("X-Consumer-Username", consumer.Username).Return(nil)
896+
mockKong.EXPECT().ServiceRequestSetHeader("X-Credential-Identifier", "1234567890").Return(nil)
897+
mockKong.EXPECT().ServiceRequestClearHeader("X-Anonymous-Consumer").Return(nil)
898+
mockKong.EXPECT().ServiceRequestClearHeader("X-Consumer-Custom-Id").Return(nil)
899+
900+
// Execution should not panic and should validate authentication despite strict audience missing
901+
pluginConfig.AccessWithInterface(mockKong)
902+
}

0 commit comments

Comments
 (0)