Skip to content

Commit afebbf2

Browse files
hickfordGustedlunny
authored
Require authentication for OAuth token refresh (#21421)
According to the OAuth spec https://datatracker.ietf.org/doc/html/rfc6749#section-6 when "Refreshing an Access Token" > The authorization server MUST ... require client authentication for confidential clients Fixes #21418 Co-authored-by: Gusted <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent f982a71 commit afebbf2

File tree

2 files changed

+68
-12
lines changed

2 files changed

+68
-12
lines changed

routers/web/auth/oauth.go

+29
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const (
4848
// TODO move error and responses to SDK or models
4949

5050
// AuthorizeErrorCode represents an error code specified in RFC 6749
51+
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
5152
type AuthorizeErrorCode string
5253

5354
const (
@@ -68,6 +69,7 @@ const (
6869
)
6970

7071
// AuthorizeError represents an error type specified in RFC 6749
72+
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
7173
type AuthorizeError struct {
7274
ErrorCode AuthorizeErrorCode `json:"error" form:"error"`
7375
ErrorDescription string
@@ -80,6 +82,7 @@ func (err AuthorizeError) Error() string {
8082
}
8183

8284
// AccessTokenErrorCode represents an error code specified in RFC 6749
85+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
8386
type AccessTokenErrorCode string
8487

8588
const (
@@ -98,6 +101,7 @@ const (
98101
)
99102

100103
// AccessTokenError represents an error response specified in RFC 6749
104+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
101105
type AccessTokenError struct {
102106
ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
103107
ErrorDescription string `json:"error_description"`
@@ -129,6 +133,7 @@ const (
129133
)
130134

131135
// AccessTokenResponse represents a successful access token response
136+
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
132137
type AccessTokenResponse struct {
133138
AccessToken string `json:"access_token"`
134139
TokenType TokenType `json:"token_type"`
@@ -663,6 +668,30 @@ func AccessTokenOAuth(ctx *context.Context) {
663668
}
664669

665670
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2.JWTSigningKey) {
671+
app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
672+
if err != nil {
673+
handleAccessTokenError(ctx, AccessTokenError{
674+
ErrorCode: AccessTokenErrorCodeInvalidClient,
675+
ErrorDescription: fmt.Sprintf("cannot load client with client id: %q", form.ClientID),
676+
})
677+
return
678+
}
679+
// "The authorization server MUST ... require client authentication for confidential clients"
680+
// https://datatracker.ietf.org/doc/html/rfc6749#section-6
681+
if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
682+
errorDescription := "invalid client secret"
683+
if form.ClientSecret == "" {
684+
errorDescription = "invalid empty client secret"
685+
}
686+
// "invalid_client ... Client authentication failed"
687+
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
688+
handleAccessTokenError(ctx, AccessTokenError{
689+
ErrorCode: AccessTokenErrorCodeInvalidClient,
690+
ErrorDescription: errorDescription,
691+
})
692+
return
693+
}
694+
666695
token, err := oauth2.ParseToken(form.RefreshToken, serverKey)
667696
if err != nil {
668697
handleAccessTokenError(ctx, AccessTokenError{

tests/integration/oauth_test.go

+39-12
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,11 @@ func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
299299
"client_secret": "inconsistent",
300300
})
301301
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
302+
resp = MakeRequest(t, req, http.StatusBadRequest)
302303
parsedError = new(auth.AccessTokenError)
303304
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
304305
assert.Equal(t, "invalid_request", string(parsedError.ErrorCode))
305-
assert.Equal(t, "client_id in request body inconsistent with Authorization header", parsedError.ErrorDescription)
306+
assert.Equal(t, "client_secret in request body inconsistent with Authorization header", parsedError.ErrorDescription)
306307
}
307308

308309
func TestRefreshTokenInvalidation(t *testing.T) {
@@ -329,32 +330,58 @@ func TestRefreshTokenInvalidation(t *testing.T) {
329330
// test without invalidation
330331
setting.OAuth2.InvalidateRefreshTokens = false
331332

332-
refreshReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
333+
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
334+
"grant_type": "refresh_token",
335+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
336+
// omit secret
337+
"redirect_uri": "a",
338+
"refresh_token": parsed.RefreshToken,
339+
})
340+
resp = MakeRequest(t, req, http.StatusBadRequest)
341+
parsedError := new(auth.AccessTokenError)
342+
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
343+
assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
344+
assert.Equal(t, "invalid empty client secret", parsedError.ErrorDescription)
345+
346+
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
347+
"grant_type": "refresh_token",
348+
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
349+
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
350+
"redirect_uri": "a",
351+
"refresh_token": "UNEXPECTED",
352+
})
353+
resp = MakeRequest(t, req, http.StatusBadRequest)
354+
parsedError = new(auth.AccessTokenError)
355+
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
356+
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
357+
assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
358+
359+
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
333360
"grant_type": "refresh_token",
334361
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
335362
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
336363
"redirect_uri": "a",
337364
"refresh_token": parsed.RefreshToken,
338365
})
339366

340-
bs, err := io.ReadAll(refreshReq.Body)
367+
bs, err := io.ReadAll(req.Body)
341368
assert.NoError(t, err)
342369

343-
refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
344-
MakeRequest(t, refreshReq, http.StatusOK)
370+
req.Body = io.NopCloser(bytes.NewReader(bs))
371+
MakeRequest(t, req, http.StatusOK)
345372

346-
refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
347-
MakeRequest(t, refreshReq, http.StatusOK)
373+
req.Body = io.NopCloser(bytes.NewReader(bs))
374+
MakeRequest(t, req, http.StatusOK)
348375

349376
// test with invalidation
350377
setting.OAuth2.InvalidateRefreshTokens = true
351-
refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
352-
MakeRequest(t, refreshReq, http.StatusOK)
378+
req.Body = io.NopCloser(bytes.NewReader(bs))
379+
MakeRequest(t, req, http.StatusOK)
353380

354381
// repeat request should fail
355-
refreshReq.Body = io.NopCloser(bytes.NewReader(bs))
356-
resp = MakeRequest(t, refreshReq, http.StatusBadRequest)
357-
parsedError := new(auth.AccessTokenError)
382+
req.Body = io.NopCloser(bytes.NewReader(bs))
383+
resp = MakeRequest(t, req, http.StatusBadRequest)
384+
parsedError = new(auth.AccessTokenError)
358385
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
359386
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
360387
assert.Equal(t, "token was already used", parsedError.ErrorDescription)

0 commit comments

Comments
 (0)