From d239adcc0b7a3b9cff46c2902a0d982042992a10 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Sun, 26 Apr 2026 15:25:58 +0300 Subject: [PATCH] (backported from v0.37.4) bitbucket,github,gitea providers update --- CHANGELOG.md | 5 +++- tools/auth/bitbucket.go | 7 ++--- tools/auth/gitea.go | 58 ++++++++++++++++++++++++++++++++++++++--- tools/auth/github.go | 16 ++++-------- 4 files changed, 68 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 466af472..657dcc88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ ## v0.22.42 -- (_Backported from v0.37.4_) ⚠️ Fixed a pre-hijacking OAuth2 linking vulnerability ([#7662](https://github.com/pocketbase/pocketbase/discussions/7662); thanks @Alardiians for reporting it privately). +- (_Backported from v0.37.4_) Adjusted Bitbucket, GitHub and Gitea/Forgejo OAuth2 providers to better reflect recent API updates and doc references. + _The providers also now always send a sepatate emails list internal request since it contains more information about the fetched email than the userinfo endpoint in order to minimize eventual linking security issues caused by custom onpremise setups (e.g. Gitea/Forgejo allows skipping the emails verification if an ENV variable is configured)._ + +- (_Backported from v0.37.4_) ⚠️ Fixed a pre-hijacking OAuth2 linking vulnerability ([#7662](https://github.com/pocketbase/pocketbase/discussions/7662)). ## v0.22.41 diff --git a/tools/auth/bitbucket.go b/tools/auth/bitbucket.go index 6381b6cd..42418ed2 100644 --- a/tools/auth/bitbucket.go +++ b/tools/auth/bitbucket.go @@ -114,8 +114,9 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) { expected := struct { Values []struct { - Email string `json:"email"` - IsPrimary bool `json:"is_primary"` + Email string `json:"email"` + IsPrimary bool `json:"is_primary"` + IsConfirmed bool `json:"is_confirmed"` } `json:"values"` }{} if err := json.Unmarshal(data, &expected); err != nil { @@ -123,7 +124,7 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) { } for _, v := range expected.Values { - if v.IsPrimary { + if v.IsPrimary && v.IsConfirmed { return v.Email, nil } } diff --git a/tools/auth/gitea.go b/tools/auth/gitea.go index b60f3c4f..9ce04b4b 100644 --- a/tools/auth/gitea.go +++ b/tools/auth/gitea.go @@ -3,6 +3,8 @@ package auth import ( "context" "encoding/json" + "fmt" + "io" "strconv" "github.com/pocketbase/pocketbase/tools/types" @@ -47,11 +49,10 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { } extracted := struct { - Id int `json:"id"` Name string `json:"full_name"` Username string `json:"login"` - Email string `json:"email"` AvatarUrl string `json:"avatar_url"` + Id int `json:"id"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err @@ -61,14 +62,65 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: strconv.Itoa(extracted.Id), Name: extracted.Name, Username: extracted.Username, - Email: extracted.Email, AvatarUrl: extracted.AvatarUrl, RawUser: rawUser, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, } + email, err := p.fetchVerifiedPrimaryEmail(token) + if err != nil { + return nil, fmt.Errorf("failed to fetch primary email: %w", err) + } + user.Email = email + user.Expiry, _ = types.ParseDateTime(token.Expiry) return user, nil } + +// fetchVerifiedPrimaryEmail sends an API request to retrieve the verified +// primary email, in case "Keep my email address private" was set. +// +// NB! This method can succeed and still return an empty email. +// Error responses that are result of insufficient scopes permissions are ignored. +// +// API reference: https://codeberg.org/api/swagger#/user/userListEmails +func (p *Gitea) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) { + client := p.Client(token) + + response, err := client.Get(p.userApiUrl + "/emails") + if err != nil { + return "", err + } + defer response.Body.Close() + + // ignore common http errors caused by insufficient scope permissions + // (the email field is optional, aka. return the auth user without it) + if response.StatusCode == 401 || response.StatusCode == 403 || response.StatusCode == 404 { + return "", nil + } + + content, err := io.ReadAll(response.Body) + if err != nil { + return "", err + } + + emails := []struct { + Email string + Verified bool + Primary bool + }{} + if err := json.Unmarshal(content, &emails); err != nil { + return "", err + } + + // extract the verified primary email + for _, email := range emails { + if email.Verified && email.Primary { + return email.Email, nil + } + } + + return "", nil +} diff --git a/tools/auth/github.go b/tools/auth/github.go index e1c2014f..31ad3b33 100644 --- a/tools/auth/github.go +++ b/tools/auth/github.go @@ -50,10 +50,9 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { extracted := struct { Login string `json:"login"` - Id int `json:"id"` Name string `json:"name"` - Email string `json:"email"` AvatarUrl string `json:"avatar_url"` + Id int `json:"id"` }{} if err := json.Unmarshal(data, &extracted); err != nil { return nil, err @@ -63,7 +62,6 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { Id: strconv.Itoa(extracted.Id), Name: extracted.Name, Username: extracted.Login, - Email: extracted.Email, AvatarUrl: extracted.AvatarUrl, RawUser: rawUser, AccessToken: token.AccessToken, @@ -72,15 +70,11 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { user.Expiry, _ = types.ParseDateTime(token.Expiry) - // in case user has set "Keep my email address private", send an - // **optional** API request to retrieve the verified primary email - if user.Email == "" { - email, err := p.fetchPrimaryEmail(token) - if err != nil { - return nil, err - } - user.Email = email + email, err := p.fetchPrimaryEmail(token) + if err != nil { + return nil, err } + user.Email = email return user, nil }