updated bitbucket,github and gitea oauth2 providers

This commit is contained in:
Gani Georgiev
2026-04-25 17:51:20 +03:00
parent 5d55fc18ee
commit dddb0a029f
4 changed files with 84 additions and 24 deletions

View File

@@ -8,6 +8,10 @@
- Fixed autocomplete selection not properly updating the underlying input value ([#7664](https://github.com/pocketbase/pocketbase/issues/7664)). - Fixed autocomplete selection not properly updating the underlying input value ([#7664](https://github.com/pocketbase/pocketbase/issues/7664)).
- Adjusted Bitbucket, GitHub and Gitea/Forgejo OAuth2 providers to better reflect recent API updates and doc references.
_The providers also now always send an extra emails_list request to fetch only the explicitly verified primary email in order to eliminate eventual security issues caused by misconfigured onpremise setups._
## v0.37.3 ## v0.37.3

View File

@@ -120,8 +120,9 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
expected := struct { expected := struct {
Values []struct { Values []struct {
Email string `json:"email"` Email string `json:"email"`
IsPrimary bool `json:"is_primary"` IsPrimary bool `json:"is_primary"`
IsConfirmed bool `json:"is_confirmed"`
} `json:"values"` } `json:"values"`
}{} }{}
if err := json.Unmarshal(data, &expected); err != nil { if err := json.Unmarshal(data, &expected); err != nil {
@@ -129,7 +130,7 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
} }
for _, v := range expected.Values { for _, v := range expected.Values {
if v.IsPrimary { if v.IsPrimary && v.IsConfirmed {
return v.Email, nil return v.Email, nil
} }
} }

View File

@@ -3,6 +3,9 @@ package auth
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io"
"strconv" "strconv"
"github.com/pocketbase/pocketbase/tools/types" "github.com/pocketbase/pocketbase/tools/types"
@@ -15,10 +18,10 @@ func init() {
var _ Provider = (*Gitea)(nil) var _ Provider = (*Gitea)(nil)
// NameGitea is the unique name of the Gitea provider. // NameGitea is the unique name of the Gitea/Forgejo provider.
const NameGitea string = "gitea" const NameGitea string = "gitea"
// Gitea allows authentication via Gitea OAuth2. // Gitea allows authentication via Gitea/Forgejo OAuth2.
type Gitea struct { type Gitea struct {
BaseProvider BaseProvider
} }
@@ -38,9 +41,9 @@ func NewGiteaProvider() *Gitea {
}} }}
} }
// FetchAuthUser returns an AuthUser instance based on Gitea's user api. // FetchAuthUser returns an AuthUser instance based on Gitea/Forgejo's user api.
// //
// API reference: https://try.gitea.io/api/swagger#/user/userGetCurrent // API reference: https://codeberg.org/api/swagger#/user/userGetCurrent
func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token) data, err := p.FetchRawUserInfo(token)
if err != nil { if err != nil {
@@ -55,26 +58,81 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
extracted := struct { extracted := struct {
Name string `json:"full_name"` Name string `json:"full_name"`
Username string `json:"login"` Username string `json:"login"`
Email string `json:"email"`
AvatarURL string `json:"avatar_url"` AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"` Id int64 `json:"id"`
Active bool `json:"active"`
}{} }{}
if err := json.Unmarshal(data, &extracted); err != nil { if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err return nil, err
} }
if !extracted.Active {
return nil, errors.New("user account is not active")
}
user := &AuthUser{ user := &AuthUser{
Id: strconv.FormatInt(extracted.Id, 10), Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name, Name: extracted.Name,
Username: extracted.Username, Username: extracted.Username,
Email: extracted.Email,
AvatarURL: extracted.AvatarURL, AvatarURL: extracted.AvatarURL,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken, 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) user.Expiry, _ = types.ParseDateTime(token.Expiry)
return user, nil 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.userInfoURL + "/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
}

View File

@@ -42,7 +42,7 @@ func NewGithubProvider() *Github {
// FetchAuthUser returns an AuthUser instance based the Github's user api. // FetchAuthUser returns an AuthUser instance based the Github's user api.
// //
// API reference: https://docs.github.com/en/rest/reference/users#get-the-authenticated-user // API reference: https://docs.github.com/en/rest/users/users?apiVersion=2026-03-10#get-the-authenticated-user
func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) { func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
data, err := p.FetchRawUserInfo(token) data, err := p.FetchRawUserInfo(token)
if err != nil { if err != nil {
@@ -55,9 +55,8 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
} }
extracted := struct { extracted := struct {
Login string `json:"login"`
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Login string `json:"login"`
AvatarURL string `json:"avatar_url"` AvatarURL string `json:"avatar_url"`
Id int64 `json:"id"` Id int64 `json:"id"`
}{} }{}
@@ -69,7 +68,6 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
Id: strconv.FormatInt(extracted.Id, 10), Id: strconv.FormatInt(extracted.Id, 10),
Name: extracted.Name, Name: extracted.Name,
Username: extracted.Login, Username: extracted.Login,
Email: extracted.Email,
AvatarURL: extracted.AvatarURL, AvatarURL: extracted.AvatarURL,
RawUser: rawUser, RawUser: rawUser,
AccessToken: token.AccessToken, AccessToken: token.AccessToken,
@@ -78,27 +76,26 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
user.Expiry, _ = types.ParseDateTime(token.Expiry) user.Expiry, _ = types.ParseDateTime(token.Expiry)
// in case user has set "Keep my email address private", send an // always send a primary email request even though the email is
// **optional** API request to retrieve the verified primary email // returned in the userinfo endpoint since the API may change and
if user.Email == "" { // enterprise setups may have configuration that could allow unverified emails
email, err := p.fetchPrimaryEmail(token) email, err := p.fetchVerifiedPrimaryEmail(token)
if err != nil { if err != nil {
return nil, err return nil, err
}
user.Email = email
} }
user.Email = email
return user, nil return user, nil
} }
// fetchPrimaryEmail sends an API request to retrieve the verified // fetchVerifiedPrimaryEmail sends an API request to retrieve the verified
// primary email, in case "Keep my email address private" was set. // primary email, in case "Keep my email address private" was set.
// //
// NB! This method can succeed and still return an empty email. // NB! This method can succeed and still return an empty email.
// Error responses that are result of insufficient scopes permissions are ignored. // Error responses that are result of insufficient scopes permissions are ignored.
// //
// API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28 // API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28#list-email-addresses-for-the-authenticated-user
func (p *Github) fetchPrimaryEmail(token *oauth2.Token) (string, error) { func (p *Github) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) {
client := p.Client(token) client := p.Client(token)
response, err := client.Get(p.userInfoURL + "/emails") response, err := client.Get(p.userInfoURL + "/emails")