Skip to content

Commit adea4ec

Browse files
authored
Add support for PKCE (#265)
* Added support for PKCE Signed-off-by: Gaurav Dasson <gaurav.dasson@gmail.com> * Added support for PKCE - Updated unit tests and fixed formatting Signed-off-by: Gaurav Dasson <gaurav.dasson@gmail.com> * Added support for PKCE - Updated keycloak setup to enable PKCE for e2e tests Signed-off-by: Gaurav Dasson <gaurav.dasson@gmail.com> * Added support for PKCE - Updated go.mod with dependencies Signed-off-by: Gaurav Dasson <gaurav.dasson@gmail.com> * Added support for PKCE - Fixed linting issues Signed-off-by: Gaurav Dasson <gaurav.dasson@gmail.com> --------- Signed-off-by: Gaurav Dasson <gaurav.dasson@gmail.com>
1 parent eb7840b commit adea4ec

File tree

10 files changed

+73
-38
lines changed

10 files changed

+73
-38
lines changed

e2e/keycloak/setup-keycloak.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ set -ex
5151
-s clientId="${CLIENT_ID}" \
5252
-s secret="${CLIENT_SECRET}" \
5353
-s "redirectUris=[\"${REDIRECT_URL}\"]" \
54+
-s "attributes={\"pkce.code.challenge.method\":\"S256\"}" \
5455
-s consentRequired=false \
5556
--server "${KEYCLOAK_SERVER}" \
5657
--realm "${REALM}" \

e2e/redis/store_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ func TestRedisAuthorizationState(t *testing.T) {
8585
State: "state",
8686
Nonce: "nonce",
8787
RequestedURL: "https://example.com",
88+
CodeVerifier: "code_verifier",
8889
}
8990
require.NoError(t, store.SetAuthorizationState(ctx, "s1", as))
9091

@@ -126,6 +127,7 @@ func TestSessionExpiration(t *testing.T) {
126127
State: "state",
127128
Nonce: "nonce",
128129
RequestedURL: "https://example.com",
130+
CodeVerifier: "code_verifier",
129131
}
130132
require.NoError(t, store.SetAuthorizationState(ctx, "s1", as))
131133
require.Eventually(t, func() bool {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/tetratelabs/run v0.3.0
1616
github.com/tetratelabs/telemetry v0.8.2
1717
golang.org/x/net v0.23.0
18+
golang.org/x/oauth2 v0.18.0
1819
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8
1920
google.golang.org/grpc v1.62.1
2021
google.golang.org/protobuf v1.33.0
@@ -74,7 +75,6 @@ require (
7475
github.com/yuin/gopher-lua v1.1.1 // indirect
7576
golang.org/x/crypto v0.21.0 // indirect
7677
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
77-
golang.org/x/oauth2 v0.18.0 // indirect
7878
golang.org/x/sys v0.18.0 // indirect
7979
golang.org/x/term v0.18.0 // indirect
8080
golang.org/x/text v0.14.0 // indirect

internal/authz/oidc.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
3131
"github.com/lestrrat-go/jwx/v2/jws"
3232
"github.com/tetratelabs/telemetry"
33+
"golang.org/x/oauth2"
3334
"google.golang.org/genproto/googleapis/rpc/status"
3435
"google.golang.org/grpc/codes"
3536

@@ -237,9 +238,10 @@ func (o *oidcHandler) redirectToIDP(ctx context.Context, log telemetry.Logger,
237238
}
238239

239240
var (
240-
sessionID = o.sessionGen.GenerateSessionID()
241-
nonce = o.sessionGen.GenerateNonce()
242-
state = o.sessionGen.GenerateState()
241+
sessionID = o.sessionGen.GenerateSessionID()
242+
nonce = o.sessionGen.GenerateNonce()
243+
state = o.sessionGen.GenerateState()
244+
codeVerifier = o.sessionGen.GenerateCodeVerifier()
243245
)
244246

245247
// Store the authorization state
@@ -251,6 +253,7 @@ func (o *oidcHandler) redirectToIDP(ctx context.Context, log telemetry.Logger,
251253
State: state,
252254
Nonce: nonce,
253255
RequestedURL: requestedURL,
256+
CodeVerifier: codeVerifier,
254257
}); err != nil {
255258
log.Error("error storing the new authorization state", err)
256259
setDenyResponse(resp, newSessionErrorResponse(), codes.Unauthenticated)
@@ -259,12 +262,14 @@ func (o *oidcHandler) redirectToIDP(ctx context.Context, log telemetry.Logger,
259262

260263
// Generate the redirect URL
261264
query := url.Values{
262-
"response_type": []string{"code"},
263-
"client_id": []string{o.config.GetClientId()},
264-
"redirect_uri": []string{o.config.GetCallbackUri()},
265-
"scope": []string{strings.Join(o.config.GetScopes(), " ")},
266-
"state": []string{state},
267-
"nonce": []string{nonce},
265+
"response_type": []string{"code"},
266+
"client_id": []string{o.config.GetClientId()},
267+
"redirect_uri": []string{o.config.GetCallbackUri()},
268+
"scope": []string{strings.Join(o.config.GetScopes(), " ")},
269+
"state": []string{state},
270+
"nonce": []string{nonce},
271+
"code_challenge": []string{oauth2.S256ChallengeFromVerifier(codeVerifier)},
272+
"code_challenge_method": []string{"S256"},
268273
}
269274
redirectURL := o.config.GetAuthorizationUri() + "?" + query.Encode()
270275

@@ -328,9 +333,10 @@ func (o *oidcHandler) retrieveTokens(ctx context.Context, log telemetry.Logger,
328333

329334
// build body
330335
form := url.Values{
331-
"grant_type": []string{"authorization_code"},
332-
"code": []string{codeFromReq},
333-
"redirect_uri": []string{o.config.GetCallbackUri()},
336+
"grant_type": []string{"authorization_code"},
337+
"code": []string{codeFromReq},
338+
"redirect_uri": []string{o.config.GetCallbackUri()},
339+
"code_verifier": []string{stateFromStore.CodeVerifier},
334340
}
335341

336342
// build headers

internal/authz/oidc_test.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/lestrrat-go/jwx/v2/jwt"
3535
"github.com/stretchr/testify/require"
3636
"github.com/tetratelabs/telemetry"
37+
"golang.org/x/oauth2"
3738
"google.golang.org/grpc/codes"
3839
"google.golang.org/grpc/test/bufconn"
3940
"google.golang.org/protobuf/proto"
@@ -125,10 +126,11 @@ var (
125126
yesterday = time.Now().Add(-24 * time.Hour)
126127
tomorrow = time.Now().Add(24 * time.Hour)
127128

128-
sessionID = "test-session-id"
129-
newSessionID = "new-session-id"
130-
newNonce = "new-nonce"
131-
newState = "new-state"
129+
sessionID = "test-session-id"
130+
newSessionID = "new-session-id"
131+
newNonce = "new-nonce"
132+
newState = "new-state"
133+
newCodeVerifier = "new-code-verifier"
132134

133135
basicOIDCConfig = &oidcv1.OIDCConfig{
134136
IdToken: &oidcv1.TokenConfig{
@@ -190,12 +192,14 @@ var (
190192
}`
191193

192194
wantRedirectParams = url.Values{
193-
"response_type": {"code"},
194-
"client_id": {"test-client-id"},
195-
"redirect_uri": {"https://localhost:443/callback"},
196-
"scope": {"openid email"},
197-
"state": {newState},
198-
"nonce": {newNonce},
195+
"response_type": {"code"},
196+
"client_id": {"test-client-id"},
197+
"redirect_uri": {"https://localhost:443/callback"},
198+
"scope": {"openid email"},
199+
"state": {newState},
200+
"nonce": {newNonce},
201+
"code_challenge": {oauth2.S256ChallengeFromVerifier(newCodeVerifier)},
202+
"code_challenge_method": {"S256"},
199203
}
200204

201205
wantRedirectBaseURI = "http://idp-test-server/auth"
@@ -228,7 +232,7 @@ func TestOIDCProcess(t *testing.T) {
228232
tlsPool := internal.NewTLSConfigPool(context.Background())
229233
h, err := NewOIDCHandler(basicOIDCConfig, tlsPool,
230234
oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool), sessions, clock,
231-
oidc.NewStaticGenerator(newSessionID, newNonce, newState))
235+
oidc.NewStaticGenerator(newSessionID, newNonce, newState, newCodeVerifier))
232236
require.NoError(t, err)
233237

234238
ctx := context.Background()
@@ -949,7 +953,7 @@ func TestOIDCProcessWithFailingSessionStore(t *testing.T) {
949953
}
950954

951955
h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool),
952-
sessions, oidc.Clock{}, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
956+
sessions, oidc.Clock{}, oidc.NewStaticGenerator(newSessionID, newNonce, newState, newCodeVerifier))
953957
require.NoError(t, err)
954958

955959
ctx := context.Background()
@@ -1094,7 +1098,8 @@ func TestOIDCProcessWithFailingJWKSProvider(t *testing.T) {
10941098
sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)}
10951099
store := sessions.Get(basicOIDCConfig)
10961100
tlsPool := internal.NewTLSConfigPool(context.Background())
1097-
h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, funcJWKSProvider, sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
1101+
h, err := NewOIDCHandler(basicOIDCConfig, tlsPool, funcJWKSProvider, sessions, clock,
1102+
oidc.NewStaticGenerator(newSessionID, newNonce, newState, newCodeVerifier))
10981103
require.NoError(t, err)
10991104

11001105
idpServer := newServer(wellKnownURIs)
@@ -1425,7 +1430,7 @@ func TestLoadWellKnownConfigError(t *testing.T) {
14251430
cfg.ConfigurationUri = "http://stopped-server/.well-known/openid-configuration"
14261431
sessions := &mockSessionStoreFactory{store: oidc.NewMemoryStore(&clock, time.Hour, time.Hour)}
14271432
_, err := NewOIDCHandler(cfg, tlsPool, oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool),
1428-
sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
1433+
sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState, newCodeVerifier))
14291434
require.Error(t, err) // Fail to retrieve the dynamic config since the test server is not running
14301435
}
14311436

@@ -1447,7 +1452,7 @@ func TestNewOIDCHandler(t *testing.T) {
14471452
t.Run(tt.name, func(t *testing.T) {
14481453

14491454
_, err := NewOIDCHandler(tt.config, tlsPool, oidc.NewJWKSProvider(newConfigFor(basicOIDCConfig), tlsPool),
1450-
sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState))
1455+
sessions, clock, oidc.NewStaticGenerator(newSessionID, newNonce, newState, newCodeVerifier))
14511456
if tt.wantErr {
14521457
require.Error(t, err)
14531458
} else {

internal/oidc/redis.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,13 @@ const (
4040
keyState = "state"
4141
keyNonce = "nonce"
4242
keyRequestedURL = "requested_url"
43+
keyCodeVerifier = "code_verifier"
4344
keyTimeAdded = "time_added"
4445
)
4546

4647
var (
4748
tokenResponseKeys = []string{keyIDToken, keyAccessToken, keyRefreshToken, keyAccessTokenExpiry, keyTimeAdded}
48-
authorizationStateKeys = []string{keyState, keyNonce, keyRequestedURL, keyTimeAdded}
49+
authorizationStateKeys = []string{keyState, keyNonce, keyRequestedURL, keyTimeAdded, keyCodeVerifier}
4950
)
5051

5152
// redisStore is an in-memory implementation of the SessionStore interface that stores
@@ -165,6 +166,7 @@ func (r *redisStore) SetAuthorizationState(ctx context.Context, sessionID string
165166
keyState: authorizationState.State,
166167
keyNonce: authorizationState.Nonce,
167168
keyRequestedURL: authorizationState.RequestedURL,
169+
keyCodeVerifier: authorizationState.CodeVerifier,
168170
}
169171

170172
if err := r.client.HMSet(ctx, sessionID, state).Err(); err != nil {
@@ -193,7 +195,7 @@ func (r *redisStore) GetAuthorizationState(ctx context.Context, sessionID string
193195
return nil, err
194196
}
195197

196-
if state.State == "" || state.Nonce == "" || state.RequestedURL == "" {
198+
if state.State == "" || state.Nonce == "" || state.RequestedURL == "" || state.CodeVerifier == "" {
197199
return nil, nil
198200
}
199201

@@ -286,6 +288,7 @@ type (
286288
State string `redis:"state"`
287289
Nonce string `redis:"nonce"`
288290
RequestedURL string `redis:"requested_url"`
291+
CodeVerifier string `redis:"code_verifier"`
289292
TimeAdded time.Time `redis:"time_added"`
290293
}
291294
)
@@ -304,5 +307,6 @@ func (r redisAuthState) AuthorizationState() *AuthorizationState {
304307
State: r.State,
305308
Nonce: r.Nonce,
306309
RequestedURL: r.RequestedURL,
310+
CodeVerifier: r.CodeVerifier,
307311
}
308312
}

internal/oidc/redis_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func TestRedisAuthorizationState(t *testing.T) {
9191
State: "state",
9292
Nonce: "nonce",
9393
RequestedURL: "requested_url",
94+
CodeVerifier: "code_verifier",
9495
}
9596
require.NoError(t, store.SetAuthorizationState(ctx, "s1", as))
9697

internal/oidc/session.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/redis/go-redis/v9"
2323
"github.com/tetratelabs/run"
2424
"github.com/tetratelabs/telemetry"
25+
"golang.org/x/oauth2"
2526

2627
configv1 "github.com/istio-ecosystem/authservice/config/gen/go/v1"
2728
oidcv1 "github.com/istio-ecosystem/authservice/config/gen/go/v1/oidc"
@@ -136,6 +137,7 @@ type SessionGenerator interface {
136137
GenerateSessionID() string
137138
GenerateNonce() string
138139
GenerateState() string
140+
GenerateCodeVerifier() string
139141
}
140142

141143
var (
@@ -151,9 +153,10 @@ type (
151153

152154
// staticGenerator is a session generator that uses static strings.
153155
staticGenerator struct {
154-
sessionID string
155-
nonce string
156-
state string
156+
sessionID string
157+
nonce string
158+
state string
159+
codeVerifier string
157160
}
158161
)
159162

@@ -176,6 +179,10 @@ func (r randomGenerator) GenerateState() string {
176179
return r.generate(32)
177180
}
178181

182+
func (r randomGenerator) GenerateCodeVerifier() string {
183+
return oauth2.GenerateVerifier()
184+
}
185+
179186
func (r *randomGenerator) generate(n int) string {
180187
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
181188
b := make([]byte, n)
@@ -186,11 +193,12 @@ func (r *randomGenerator) generate(n int) string {
186193
}
187194

188195
// NewStaticGenerator creates a new static session generator.
189-
func NewStaticGenerator(sessionID, nonce, state string) SessionGenerator {
196+
func NewStaticGenerator(sessionID, nonce, state, codeVerifier string) SessionGenerator {
190197
return &staticGenerator{
191-
sessionID: sessionID,
192-
nonce: nonce,
193-
state: state,
198+
sessionID: sessionID,
199+
nonce: nonce,
200+
state: state,
201+
codeVerifier: codeVerifier,
194202
}
195203
}
196204

@@ -205,3 +213,7 @@ func (s staticGenerator) GenerateNonce() string {
205213
func (s staticGenerator) GenerateState() string {
206214
return s.state
207215
}
216+
217+
func (s staticGenerator) GenerateCodeVerifier() string {
218+
return s.codeVerifier
219+
}

internal/oidc/session_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,17 @@ func TestSessionGenerator(t *testing.T) {
131131
require.NotEqual(t, sg.GenerateSessionID(), sg.GenerateSessionID())
132132
require.NotEqual(t, sg.GenerateState(), sg.GenerateState())
133133
require.NotEqual(t, sg.GenerateNonce(), sg.GenerateNonce())
134+
require.NotEqual(t, sg.GenerateCodeVerifier(), sg.GenerateCodeVerifier())
134135
})
135136
t.Run("static", func(t *testing.T) {
136-
sg := NewStaticGenerator("sessionid", "nonce", "state")
137+
sg := NewStaticGenerator("sessionid", "nonce", "state", "codeverifier")
137138
require.Equal(t, sg.GenerateSessionID(), sg.GenerateSessionID())
138139
require.Equal(t, sg.GenerateState(), sg.GenerateState())
139140
require.Equal(t, sg.GenerateNonce(), sg.GenerateNonce())
141+
require.Equal(t, sg.GenerateCodeVerifier(), sg.GenerateCodeVerifier())
140142
require.Equal(t, "sessionid", sg.GenerateSessionID())
141143
require.Equal(t, "state", sg.GenerateState())
142144
require.Equal(t, "nonce", sg.GenerateNonce())
145+
require.Equal(t, "codeverifier", sg.GenerateCodeVerifier())
143146
})
144147
}

internal/oidc/state.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ type AuthorizationState struct {
1919
State string
2020
Nonce string
2121
RequestedURL string
22+
CodeVerifier string
2223
}

0 commit comments

Comments
 (0)