Skip to content

Commit e3463df

Browse files
silverwindclaude
andcommitted
refactor(oauth): use x/oauth2 S256ChallengeFromVerifier and tighten tests
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
1 parent 199a544 commit e3463df

2 files changed

Lines changed: 38 additions & 109 deletions

File tree

models/auth/oauth2.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ package auth
55

66
import (
77
"context"
8-
"crypto/sha256"
98
"crypto/subtle"
109
"encoding/base32"
11-
"encoding/base64"
1210
"errors"
1311
"fmt"
1412
"net"
@@ -25,14 +23,18 @@ import (
2523

2624
uuid "github.com/google/uuid"
2725
"golang.org/x/crypto/bcrypt"
26+
"golang.org/x/oauth2"
2827
"xorm.io/builder"
2928
"xorm.io/xorm"
3029
)
3130

3231
// Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
3332
const oauth2AuthorizationCodeValidity = 10 * time.Minute
3433

35-
var ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
34+
var (
35+
ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
36+
ErrOAuth2GrantStaleCounter = errors.New("oauth2 grant state changed during token refresh")
37+
)
3638

3739
// OAuth2Application represents an OAuth2 client (RFC 6749)
3840
type OAuth2Application struct {
@@ -442,8 +444,7 @@ func (code *OAuth2AuthorizationCode) requiresCodeVerifier() bool {
442444
func deriveCodeChallenge(method, verifier string) (string, bool) {
443445
switch method {
444446
case "S256":
445-
hash := sha256.Sum256([]byte(verifier))
446-
return base64.RawURLEncoding.EncodeToString(hash[:]), true
447+
return oauth2.S256ChallengeFromVerifier(verifier), true
447448
case "plain":
448449
return verifier, true
449450
default:
@@ -537,7 +538,7 @@ func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
537538
return err
538539
}
539540
if affected == 0 {
540-
return errors.New("grant state changed during token refresh")
541+
return ErrOAuth2GrantStaleCounter
541542
}
542543
grant.Counter++
543544
return nil

models/auth/oauth2_test.go

Lines changed: 31 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
package auth_test
55

66
import (
7-
"crypto/sha256"
8-
"encoding/base64"
97
"testing"
108
"time"
119

@@ -15,6 +13,7 @@ import (
1513

1614
"github.com/stretchr/testify/assert"
1715
"github.com/stretchr/testify/require"
16+
"golang.org/x/oauth2"
1817
)
1918

2019
func TestOAuth2AuthorizationCode(t *testing.T) {
@@ -143,6 +142,12 @@ func TestOAuth2Application_ContainsRedirectURI_ASCIIOnlyNormalization(t *testing
143142
redirectURI: "https://signİn.example.test/callback",
144143
allowed: false,
145144
},
145+
{
146+
name: "loopback-strips-port",
147+
registered: []string{"http://127.0.0.1/callback"},
148+
redirectURI: "http://127.0.0.1:12345/callback",
149+
allowed: true,
150+
},
146151
}
147152

148153
for _, tc := range testCases {
@@ -237,8 +242,7 @@ func TestOAuth2Grant_IncreaseCounterRejectsStaleCounter(t *testing.T) {
237242

238243
assert.NoError(t, grant.IncreaseCounter(t.Context()))
239244
err := stale.IncreaseCounter(t.Context())
240-
assert.Error(t, err)
241-
assert.Contains(t, err.Error(), "grant state changed during token refresh")
245+
assert.ErrorIs(t, err, auth_model.ErrOAuth2GrantStaleCounter)
242246
}
243247

244248
func TestOAuth2Grant_ScopeContains(t *testing.T) {
@@ -285,112 +289,36 @@ func TestRevokeOAuth2Grant(t *testing.T) {
285289
//////////////////// Authorization Code
286290

287291
func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) {
288-
s256Verifier := "phase4-s256-verifier"
289-
s256Sum := sha256.Sum256([]byte(s256Verifier))
290-
missingVerifierSeed := "phase4-verifier-without-body"
291-
missingVerifierSum := sha256.Sum256([]byte(missingVerifierSeed))
292+
s256Verifier := "s256-verifier"
293+
s256Challenge := oauth2.S256ChallengeFromVerifier(s256Verifier)
294+
missingVerifierChallenge := oauth2.S256ChallengeFromVerifier("verifier-not-supplied")
292295

293296
testCases := []struct {
294-
name string
295-
code *auth_model.OAuth2AuthorizationCode
296-
verifier string
297-
valid bool
297+
name string
298+
method string
299+
challenge string
300+
verifier string
301+
valid bool
298302
}{
299-
{
300-
name: "plain-success",
301-
code: &auth_model.OAuth2AuthorizationCode{
302-
CodeChallengeMethod: "plain",
303-
CodeChallenge: "plain-phase4-secret",
304-
},
305-
verifier: "plain-phase4-secret",
306-
valid: true,
307-
},
308-
{
309-
name: "plain-failure",
310-
code: &auth_model.OAuth2AuthorizationCode{
311-
CodeChallengeMethod: "plain",
312-
CodeChallenge: "plain-phase4-secret",
313-
},
314-
verifier: "ierwgjoergjio",
315-
valid: false,
316-
},
317-
{
318-
name: "s256-success",
319-
code: &auth_model.OAuth2AuthorizationCode{
320-
CodeChallengeMethod: "S256",
321-
CodeChallenge: base64.RawURLEncoding.EncodeToString(s256Sum[:]),
322-
},
323-
verifier: s256Verifier,
324-
valid: true,
325-
},
326-
{
327-
name: "s256-failure",
328-
code: &auth_model.OAuth2AuthorizationCode{
329-
CodeChallengeMethod: "S256",
330-
CodeChallenge: base64.RawURLEncoding.EncodeToString(s256Sum[:]),
331-
},
332-
verifier: "wiogjerogorewngoenrgoiuenorg",
333-
valid: false,
334-
},
335-
{
336-
name: "unsupported-method",
337-
code: &auth_model.OAuth2AuthorizationCode{
338-
CodeChallengeMethod: "monkey",
339-
CodeChallenge: "foiwgjioriogeiogjerger",
340-
},
341-
verifier: "foiwgjioriogeiogjerger",
342-
valid: false,
343-
},
344-
{
345-
name: "no-pkce-configured",
346-
code: &auth_model.OAuth2AuthorizationCode{
347-
CodeChallengeMethod: "",
348-
CodeChallenge: "",
349-
},
350-
verifier: "",
351-
valid: true,
352-
},
353-
{
354-
name: "s256-missing-verifier",
355-
code: &auth_model.OAuth2AuthorizationCode{
356-
CodeChallengeMethod: "S256",
357-
CodeChallenge: base64.RawURLEncoding.EncodeToString(missingVerifierSum[:]),
358-
},
359-
verifier: "",
360-
valid: false,
361-
},
362-
{
363-
name: "plain-missing-verifier",
364-
code: &auth_model.OAuth2AuthorizationCode{
365-
CodeChallengeMethod: "plain",
366-
CodeChallenge: "plain-phase4-secret",
367-
},
368-
verifier: "",
369-
valid: false,
370-
},
371-
{
372-
name: "missing-method-with-challenge",
373-
code: &auth_model.OAuth2AuthorizationCode{
374-
CodeChallengeMethod: "",
375-
CodeChallenge: "foierjiogerogerg",
376-
},
377-
verifier: "",
378-
valid: false,
379-
},
380-
{
381-
name: "missing-method-rejects-even-matching-verifier",
382-
code: &auth_model.OAuth2AuthorizationCode{
383-
CodeChallengeMethod: "",
384-
CodeChallenge: "foierjiogerogerg",
385-
},
386-
verifier: "foierjiogerogerg",
387-
valid: false,
388-
},
303+
{"plain-success", "plain", "plain-secret", "plain-secret", true},
304+
{"plain-failure", "plain", "plain-secret", "ierwgjoergjio", false},
305+
{"s256-success", "S256", s256Challenge, s256Verifier, true},
306+
{"s256-failure", "S256", s256Challenge, "wiogjerogorewngoenrgoiuenorg", false},
307+
{"unsupported-method", "monkey", "foiwgjioriogeiogjerger", "foiwgjioriogeiogjerger", false},
308+
{"no-pkce-configured", "", "", "", true},
309+
{"s256-missing-verifier", "S256", missingVerifierChallenge, "", false},
310+
{"plain-missing-verifier", "plain", "plain-secret", "", false},
311+
{"missing-method-with-challenge", "", "foierjiogerogerg", "", false},
312+
{"missing-method-rejects-even-matching-verifier", "", "foierjiogerogerg", "foierjiogerogerg", false},
389313
}
390314

391315
for _, tc := range testCases {
392316
t.Run(tc.name, func(t *testing.T) {
393-
assert.Equal(t, tc.valid, tc.code.ValidateCodeChallenge(tc.verifier))
317+
code := &auth_model.OAuth2AuthorizationCode{
318+
CodeChallengeMethod: tc.method,
319+
CodeChallenge: tc.challenge,
320+
}
321+
assert.Equal(t, tc.valid, code.ValidateCodeChallenge(tc.verifier))
394322
})
395323
}
396324
}

0 commit comments

Comments
 (0)