Skip to content

Commit c51628e

Browse files
authoredMar 24, 2025··
feat(op): always verify code challenge when available (#721)
Finally the RFC Best Current Practice for OAuth 2.0 Security has been approved. According to the RFC: > Authorization servers MUST support PKCE [RFC7636]. > > If a client sends a valid PKCE code_challenge parameter in the authorization request, the authorization server MUST enforce the correct usage of code_verifier at the token endpoint. Isn’t it time we strengthen PKCE support a bit more? This PR updates the logic so that PKCE is always verified, even when the Auth Method is not "none".
1 parent 7096406 commit c51628e

File tree

6 files changed

+45
-15
lines changed

6 files changed

+45
-15
lines changed
 

‎example/client/app/app.go

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log/slog"
88
"net/http"
99
"os"
10+
"strconv"
1011
"strings"
1112
"sync/atomic"
1213
"time"
@@ -34,6 +35,14 @@ func main() {
3435
scopes := strings.Split(os.Getenv("SCOPES"), " ")
3536
responseMode := os.Getenv("RESPONSE_MODE")
3637

38+
var pkce bool
39+
if pkceEnv, ok := os.LookupEnv("PKCE"); ok {
40+
var err error
41+
pkce, err = strconv.ParseBool(pkceEnv)
42+
if err != nil {
43+
logrus.Fatalf("error parsing PKCE %s", err.Error())
44+
}
45+
}
3746
redirectURI := fmt.Sprintf("http://localhost:%v%v", port, callbackPath)
3847
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
3948

@@ -64,6 +73,9 @@ func main() {
6473
if keyPath != "" {
6574
options = append(options, rp.WithJWTProfile(rp.SignerFromKeyPath(keyPath)))
6675
}
76+
if pkce {
77+
options = append(options, rp.WithPKCE(cookieHandler))
78+
}
6779

6880
// One can add a logger to the context,
6981
// pre-defining log attributes as required.

‎example/server/exampleop/templates/login.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@
2525
<button type="submit">Login</button>
2626
</form>
2727
</body>
28-
</html>`
29-
{{- end }}
28+
</html>
29+
{{- end }}

‎example/server/storage/oidc.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const (
1818
// CustomClaim is an example for how to return custom claims with this library
1919
CustomClaim = "custom_claim"
2020

21-
// CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage
21+
// CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchange
2222
CustomScopeImpersonatePrefix = "custom_scope:impersonate:"
2323
)
2424

@@ -143,6 +143,14 @@ func MaxAgeToInternal(maxAge *uint) *time.Duration {
143143
}
144144

145145
func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest {
146+
var codeChallenge *OIDCCodeChallenge
147+
if authReq.CodeChallenge != "" {
148+
codeChallenge = &OIDCCodeChallenge{
149+
Challenge: authReq.CodeChallenge,
150+
Method: string(authReq.CodeChallengeMethod),
151+
}
152+
}
153+
146154
return &AuthRequest{
147155
CreationDate: time.Now(),
148156
ApplicationID: authReq.ClientID,
@@ -157,10 +165,7 @@ func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthReques
157165
ResponseType: authReq.ResponseType,
158166
ResponseMode: authReq.ResponseMode,
159167
Nonce: authReq.Nonce,
160-
CodeChallenge: &OIDCCodeChallenge{
161-
Challenge: authReq.CodeChallenge,
162-
Method: string(authReq.CodeChallengeMethod),
163-
},
168+
CodeChallenge: codeChallenge,
164169
}
165170
}
166171

‎pkg/op/op_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func TestRoutes(t *testing.T) {
102102
authReq, err := storage.CreateAuthRequest(ctx, oidcAuthReq, "id1")
103103
require.NoError(t, err)
104104
storage.AuthRequestDone(authReq.GetID())
105+
storage.SaveAuthCode(ctx, authReq.GetID(), "123")
105106

106107
accessToken, refreshToken, _, err := op.CreateAccessToken(ctx, authReq, op.AccessTokenTypeBearer, testProvider, client, "")
107108
require.NoError(t, err)

‎pkg/op/server_http_routes_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func TestServerRoutes(t *testing.T) {
130130
"client_id": client.GetID(),
131131
"client_secret": "secret",
132132
"redirect_uri": "https://example.com",
133-
"code": "123",
133+
"code": "abc",
134134
},
135135
wantCode: http.StatusBadRequest,
136136
json: `{"error":"invalid_grant", "error_description":"invalid code"}`,

‎pkg/op/token_code.go

+19-7
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest,
7474
ctx, span := tracer.Start(ctx, "AuthorizeCodeClient")
7575
defer span.End()
7676

77+
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code)
78+
if err != nil {
79+
return nil, nil, err
80+
}
81+
82+
codeChallenge := request.GetCodeChallenge()
83+
if codeChallenge != nil {
84+
err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, codeChallenge)
85+
86+
if err != nil {
87+
return nil, nil, err
88+
}
89+
}
90+
7791
if tokenReq.ClientAssertionType == oidc.ClientAssertionTypeJWTAssertion {
7892
jwtExchanger, ok := exchanger.(JWTAuthorizationGrantExchanger)
7993
if !ok || !exchanger.AuthMethodPrivateKeyJWTSupported() {
@@ -83,9 +97,9 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest,
8397
if err != nil {
8498
return nil, nil, err
8599
}
86-
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code)
87100
return request, client, err
88101
}
102+
89103
client, err = exchanger.Storage().GetClientByClientID(ctx, tokenReq.ClientID)
90104
if err != nil {
91105
return nil, nil, oidc.ErrInvalidClient().WithParent(err)
@@ -94,12 +108,10 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest,
94108
return nil, nil, oidc.ErrInvalidClient().WithDescription("private_key_jwt not allowed for this client")
95109
}
96110
if client.AuthMethod() == oidc.AuthMethodNone {
97-
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code)
98-
if err != nil {
99-
return nil, nil, err
111+
if codeChallenge == nil {
112+
return nil, nil, oidc.ErrInvalidRequest().WithDescription("PKCE required")
100113
}
101-
err = AuthorizeCodeChallenge(tokenReq.CodeVerifier, request.GetCodeChallenge())
102-
return request, client, err
114+
return request, client, nil
103115
}
104116
if client.AuthMethod() == oidc.AuthMethodPost && !exchanger.AuthMethodPostSupported() {
105117
return nil, nil, oidc.ErrInvalidClient().WithDescription("auth_method post not supported")
@@ -108,7 +120,7 @@ func AuthorizeCodeClient(ctx context.Context, tokenReq *oidc.AccessTokenRequest,
108120
if err != nil {
109121
return nil, nil, err
110122
}
111-
request, err = AuthRequestByCode(ctx, exchanger.Storage(), tokenReq.Code)
123+
112124
return request, client, err
113125
}
114126

0 commit comments

Comments
 (0)
Please sign in to comment.