loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'fix: PKCE only for OpenID Connect authentication sources' (#4094) from oliverpool/forgejo:pkce_goth_fix into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4094
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>

+85 -17
+1 -1
release-notes/8.0.0/feat/3307.md
··· 1 - Support [Proof Key for Code Exchange (PKCE - RFC7636)](https://www.rfc-editor.org/rfc/rfc7636) for external login sources using the OAuth2 flow. 1 + Support [Proof Key for Code Exchange (PKCE - RFC7636)](https://www.rfc-editor.org/rfc/rfc7636) for external login using the OpenID Connect authentication source.
+19 -2
routers/web/auth/oauth.go
··· 44 44 "github.com/golang-jwt/jwt/v5" 45 45 "github.com/markbates/goth" 46 46 "github.com/markbates/goth/gothic" 47 + "github.com/markbates/goth/providers/fitbit" 48 + "github.com/markbates/goth/providers/openidConnect" 49 + "github.com/markbates/goth/providers/zoom" 47 50 go_oauth2 "golang.org/x/oauth2" 48 51 ) 49 52 ··· 888 891 return 889 892 } 890 893 891 - codeChallenge, err := generateCodeChallenge(ctx) 894 + codeChallenge, err := generateCodeChallenge(ctx, provider) 892 895 if err != nil { 893 896 ctx.ServerError("SignIn", fmt.Errorf("could not generate code_challenge: %w", err)) 894 897 return ··· 1238 1241 } 1239 1242 1240 1243 // generateCodeChallenge stores a code verifier in the session and returns a S256 code challenge for PKCE 1241 - func generateCodeChallenge(ctx *context.Context) (codeChallenge string, err error) { 1244 + func generateCodeChallenge(ctx *context.Context, provider string) (codeChallenge string, err error) { 1245 + // the `code_verifier` is only forwarded by specific providers 1246 + // https://codeberg.org/forgejo/forgejo/issues/4033 1247 + p, ok := goth.GetProviders()[provider] 1248 + if !ok { 1249 + return "", nil 1250 + } 1251 + switch p.(type) { 1252 + default: 1253 + return "", nil 1254 + case *openidConnect.Provider, *fitbit.Provider, *zoom.Provider: 1255 + // those providers forward the `code_verifier` 1256 + // a code_challenge can be generated 1257 + } 1258 + 1242 1259 codeVerifier, err := util.CryptoRandomString(43) // 256/log2(62) = 256 bits of entropy (each char having log2(62) of randomness) 1243 1260 if err != nil { 1244 1261 return "", err
+9 -2
tests/integration/integration_test.go
··· 54 54 gouuid "github.com/google/uuid" 55 55 "github.com/markbates/goth" 56 56 "github.com/markbates/goth/gothic" 57 - goth_gitlab "github.com/markbates/goth/providers/github" 58 - goth_github "github.com/markbates/goth/providers/gitlab" 57 + goth_github "github.com/markbates/goth/providers/github" 58 + goth_gitlab "github.com/markbates/goth/providers/gitlab" 59 59 "github.com/santhosh-tekuri/jsonschema/v5" 60 60 "github.com/stretchr/testify/assert" 61 61 ) ··· 323 323 "name": name, 324 324 "is_active": "on", 325 325 } 326 + } 327 + 328 + func authSourcePayloadOpenIDConnect(name, appURL string) map[string]string { 329 + payload := authSourcePayloadOAuth2(name) 330 + payload["oauth2_provider"] = "openidConnect" 331 + payload["open_id_connect_auto_discovery_url"] = appURL + ".well-known/openid-configuration" 332 + return payload 326 333 } 327 334 328 335 func authSourcePayloadGitLab(name string) map[string]string {
+56 -12
tests/integration/oauth_test.go
··· 532 532 assert.Greater(t, userAfterLogin.LastLoginUnix, userGitLab.LastLoginUnix) 533 533 } 534 534 535 - func TestSignInOAuthCallbackPKCE(t *testing.T) { 535 + func TestSignInOAuthCallbackWithoutPKCEWhenUnsupported(t *testing.T) { 536 + // https://codeberg.org/forgejo/forgejo/issues/4033 536 537 defer tests.PrepareTestEnv(t)() 537 538 538 539 // Setup authentication source ··· 557 558 resp := session.MakeRequest(t, req, http.StatusTemporaryRedirect) 558 559 dest, err := url.Parse(resp.Header().Get("Location")) 559 560 assert.NoError(t, err) 560 - assert.Equal(t, "S256", dest.Query().Get("code_challenge_method")) 561 - codeChallenge := dest.Query().Get("code_challenge") 562 - assert.NotEmpty(t, codeChallenge) 561 + assert.Empty(t, dest.Query().Get("code_challenge_method")) 562 + assert.Empty(t, dest.Query().Get("code_challenge")) 563 563 564 564 // callback (to check the initial code_challenge) 565 565 defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { 566 - codeVerifier := req.URL.Query().Get("code_verifier") 567 - assert.NotEmpty(t, codeVerifier) 568 - assert.Greater(t, len(codeVerifier), 40, codeVerifier) 569 - 570 - sha2 := sha256.New() 571 - io.WriteString(sha2, codeVerifier) 572 - assert.Equal(t, codeChallenge, base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))) 573 - 566 + assert.Empty(t, req.URL.Query().Get("code_verifier")) 574 567 return goth.User{ 575 568 Provider: gitlabName, 576 569 UserID: userGitLabUserID, ··· 581 574 resp = session.MakeRequest(t, req, http.StatusSeeOther) 582 575 assert.Equal(t, "/", test.RedirectURL(resp)) 583 576 unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userGitLab.ID}) 577 + } 578 + 579 + func TestSignInOAuthCallbackPKCE(t *testing.T) { 580 + onGiteaRun(t, func(t *testing.T, u *url.URL) { 581 + // Setup authentication source 582 + sourceName := "oidc" 583 + authSource := addAuthSource(t, authSourcePayloadOpenIDConnect(sourceName, u.String())) 584 + // Create a user as if it had been previously been created by the authentication source. 585 + userID := "5678" 586 + user := &user_model.User{ 587 + Name: "oidc.user", 588 + Email: "oidc.user@example.com", 589 + Passwd: "oidc.userpassword", 590 + Type: user_model.UserTypeIndividual, 591 + LoginType: auth_model.OAuth2, 592 + LoginSource: authSource.ID, 593 + LoginName: userID, 594 + } 595 + defer createUser(context.Background(), t, user)() 596 + 597 + // initial redirection (to generate the code_challenge) 598 + session := emptyTestSession(t) 599 + req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s", sourceName)) 600 + resp := session.MakeRequest(t, req, http.StatusTemporaryRedirect) 601 + dest, err := url.Parse(resp.Header().Get("Location")) 602 + assert.NoError(t, err) 603 + assert.Equal(t, "S256", dest.Query().Get("code_challenge_method")) 604 + codeChallenge := dest.Query().Get("code_challenge") 605 + assert.NotEmpty(t, codeChallenge) 606 + 607 + // callback (to check the initial code_challenge) 608 + defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) { 609 + codeVerifier := req.URL.Query().Get("code_verifier") 610 + assert.NotEmpty(t, codeVerifier) 611 + assert.Greater(t, len(codeVerifier), 40, codeVerifier) 612 + 613 + sha2 := sha256.New() 614 + io.WriteString(sha2, codeVerifier) 615 + assert.Equal(t, codeChallenge, base64.RawURLEncoding.EncodeToString(sha2.Sum(nil))) 616 + 617 + return goth.User{ 618 + Provider: sourceName, 619 + UserID: userID, 620 + Email: user.Email, 621 + }, nil 622 + })() 623 + req = NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", sourceName)) 624 + resp = session.MakeRequest(t, req, http.StatusSeeOther) 625 + assert.Equal(t, "/", test.RedirectURL(resp)) 626 + unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID}) 627 + }) 584 628 } 585 629 586 630 func TestSignInOAuthCallbackRedirectToEscaping(t *testing.T) {