Adding upstream version 0.28.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
88f1d47ab6
commit
e28c88ef14
933 changed files with 194711 additions and 0 deletions
166
tools/auth/apple.go
Normal file
166
tools/auth/apple.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameApple] = wrapFactory(NewAppleProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Apple)(nil)
|
||||
|
||||
// NameApple is the unique name of the Apple provider.
|
||||
const NameApple string = "apple"
|
||||
|
||||
// Apple allows authentication via Apple OAuth2.
|
||||
//
|
||||
// [OIDC differences]: https://bitbucket.org/openid/connect/src/master/How-Sign-in-with-Apple-differs-from-OpenID-Connect.md
|
||||
type Apple struct {
|
||||
BaseProvider
|
||||
|
||||
jwksURL string
|
||||
}
|
||||
|
||||
// NewAppleProvider creates a new Apple provider instance with some defaults.
|
||||
func NewAppleProvider() *Apple {
|
||||
return &Apple{
|
||||
BaseProvider: BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Apple",
|
||||
pkce: true,
|
||||
scopes: []string{"name", "email"},
|
||||
authURL: "https://appleid.apple.com/auth/authorize",
|
||||
tokenURL: "https://appleid.apple.com/auth/token",
|
||||
},
|
||||
jwksURL: "https://appleid.apple.com/auth/keys",
|
||||
}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the provided token.
|
||||
//
|
||||
// API reference: https://developer.apple.com/documentation/sign_in_with_apple/tokenresponse.
|
||||
func (p *Apple) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified any `json:"email_verified"` // could be string or bool
|
||||
User struct {
|
||||
Name struct {
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
} `json:"name"`
|
||||
} `json:"user"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if cast.ToBool(extracted.EmailVerified) {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
if user.Name == "" {
|
||||
user.Name = strings.TrimSpace(extracted.User.Name.FirstName + " " + extracted.User.Name.LastName)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface.
|
||||
//
|
||||
// Apple doesn't have a UserInfo endpoint and claims about users
|
||||
// are instead included in the "id_token" (https://openid.net/specs/openid-connect-core-1_0.html#id_tokenExample)
|
||||
func (p *Apple) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
idToken, _ := token.Extra("id_token").(string)
|
||||
|
||||
claims, err := p.parseAndVerifyIdToken(idToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Apple only returns the user object the first time the user authorizes the app
|
||||
// https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple#3331292
|
||||
rawUser, _ := token.Extra("user").(string)
|
||||
if rawUser != "" {
|
||||
user := map[string]any{}
|
||||
err = json.Unmarshal([]byte(rawUser), &user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims["user"] = user
|
||||
}
|
||||
|
||||
return json.Marshal(claims)
|
||||
}
|
||||
|
||||
func (p *Apple) parseAndVerifyIdToken(idToken string) (jwt.MapClaims, error) {
|
||||
if idToken == "" {
|
||||
return nil, errors.New("empty id_token")
|
||||
}
|
||||
|
||||
// extract the token header params and claims
|
||||
// ---
|
||||
claims := jwt.MapClaims{}
|
||||
t, _, err := jwt.NewParser().ParseUnverified(idToken, claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate common claims per https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user#3383769
|
||||
// ---
|
||||
jwtValidator := jwt.NewValidator(
|
||||
jwt.WithExpirationRequired(),
|
||||
jwt.WithIssuedAt(),
|
||||
jwt.WithLeeway(idTokenLeeway),
|
||||
jwt.WithIssuer("https://appleid.apple.com"),
|
||||
jwt.WithAudience(p.clientId),
|
||||
)
|
||||
err = jwtValidator.Validate(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate id_token signature
|
||||
//
|
||||
// note: this step could be technically considered optional because we trust
|
||||
// the token which is a result of direct TLS communication with the provider
|
||||
// (see also https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation)
|
||||
// ---
|
||||
kid, _ := t.Header["kid"].(string)
|
||||
err = validateIdTokenSignature(p.ctx, idToken, p.jwksURL, kid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
157
tools/auth/auth.go
Normal file
157
tools/auth/auth.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// ProviderFactoryFunc defines a function for initializing a new OAuth2 provider.
|
||||
type ProviderFactoryFunc func() Provider
|
||||
|
||||
// Providers defines a map with all of the available OAuth2 providers.
|
||||
//
|
||||
// To register a new provider append a new entry in the map.
|
||||
var Providers = map[string]ProviderFactoryFunc{}
|
||||
|
||||
// NewProviderByName returns a new preconfigured provider instance by its name identifier.
|
||||
func NewProviderByName(name string) (Provider, error) {
|
||||
factory, ok := Providers[name]
|
||||
if !ok {
|
||||
return nil, errors.New("missing provider " + name)
|
||||
}
|
||||
|
||||
return factory(), nil
|
||||
}
|
||||
|
||||
// Provider defines a common interface for an OAuth2 client.
|
||||
type Provider interface {
|
||||
// Context returns the context associated with the provider (if any).
|
||||
Context() context.Context
|
||||
|
||||
// SetContext assigns the specified context to the current provider.
|
||||
SetContext(ctx context.Context)
|
||||
|
||||
// PKCE indicates whether the provider can use the PKCE flow.
|
||||
PKCE() bool
|
||||
|
||||
// SetPKCE toggles the state whether the provider can use the PKCE flow or not.
|
||||
SetPKCE(enable bool)
|
||||
|
||||
// DisplayName usually returns provider name as it is officially written
|
||||
// and it could be used directly in the UI.
|
||||
DisplayName() string
|
||||
|
||||
// SetDisplayName sets the provider's display name.
|
||||
SetDisplayName(displayName string)
|
||||
|
||||
// Scopes returns the provider access permissions that will be requested.
|
||||
Scopes() []string
|
||||
|
||||
// SetScopes sets the provider access permissions that will be requested later.
|
||||
SetScopes(scopes []string)
|
||||
|
||||
// ClientId returns the provider client's app ID.
|
||||
ClientId() string
|
||||
|
||||
// SetClientId sets the provider client's ID.
|
||||
SetClientId(clientId string)
|
||||
|
||||
// ClientSecret returns the provider client's app secret.
|
||||
ClientSecret() string
|
||||
|
||||
// SetClientSecret sets the provider client's app secret.
|
||||
SetClientSecret(secret string)
|
||||
|
||||
// RedirectURL returns the end address to redirect the user
|
||||
// going through the OAuth flow.
|
||||
RedirectURL() string
|
||||
|
||||
// SetRedirectURL sets the provider's RedirectURL.
|
||||
SetRedirectURL(url string)
|
||||
|
||||
// AuthURL returns the provider's authorization service url.
|
||||
AuthURL() string
|
||||
|
||||
// SetAuthURL sets the provider's AuthURL.
|
||||
SetAuthURL(url string)
|
||||
|
||||
// TokenURL returns the provider's token exchange service url.
|
||||
TokenURL() string
|
||||
|
||||
// SetTokenURL sets the provider's TokenURL.
|
||||
SetTokenURL(url string)
|
||||
|
||||
// UserInfoURL returns the provider's user info api url.
|
||||
UserInfoURL() string
|
||||
|
||||
// SetUserInfoURL sets the provider's UserInfoURL.
|
||||
SetUserInfoURL(url string)
|
||||
|
||||
// Extra returns a shallow copy of any custom config data
|
||||
// that the provider may be need.
|
||||
Extra() map[string]any
|
||||
|
||||
// SetExtra updates the provider's custom config data.
|
||||
SetExtra(data map[string]any)
|
||||
|
||||
// Client returns an http client using the provided token.
|
||||
Client(token *oauth2.Token) *http.Client
|
||||
|
||||
// BuildAuthURL returns a URL to the provider's consent page
|
||||
// that asks for permissions for the required scopes explicitly.
|
||||
BuildAuthURL(state string, opts ...oauth2.AuthCodeOption) string
|
||||
|
||||
// FetchToken converts an authorization code to token.
|
||||
FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
||||
|
||||
// FetchRawUserInfo requests and marshalizes into `result` the
|
||||
// the OAuth user api response.
|
||||
FetchRawUserInfo(token *oauth2.Token) ([]byte, error)
|
||||
|
||||
// FetchAuthUser is similar to FetchRawUserInfo, but normalizes and
|
||||
// marshalizes the user api response into a standardized AuthUser struct.
|
||||
FetchAuthUser(token *oauth2.Token) (user *AuthUser, err error)
|
||||
}
|
||||
|
||||
// wrapFactory is a helper that wraps a Provider specific factory
|
||||
// function and returns its result as Provider interface.
|
||||
func wrapFactory[T Provider](factory func() T) ProviderFactoryFunc {
|
||||
return func() Provider {
|
||||
return factory()
|
||||
}
|
||||
}
|
||||
|
||||
// AuthUser defines a standardized OAuth2 user data structure.
|
||||
type AuthUser struct {
|
||||
Expiry types.DateTime `json:"expiry"`
|
||||
RawUser map[string]any `json:"rawUser"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatarURL"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
|
||||
// @todo
|
||||
// deprecated: use AvatarURL instead
|
||||
// AvatarUrl will be removed after dropping v0.22 support
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements the [json.Marshaler] interface.
|
||||
//
|
||||
// @todo remove after dropping v0.22 support
|
||||
func (au AuthUser) MarshalJSON() ([]byte, error) {
|
||||
type alias AuthUser // prevent recursion
|
||||
|
||||
au2 := alias(au)
|
||||
au2.AvatarUrl = au.AvatarURL // ensure that the legacy field is populated
|
||||
|
||||
return json.Marshal(au2)
|
||||
}
|
299
tools/auth/auth_test.go
Normal file
299
tools/auth/auth_test.go
Normal file
|
@ -0,0 +1,299 @@
|
|||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
)
|
||||
|
||||
func TestProvidersCount(t *testing.T) {
|
||||
expected := 30
|
||||
|
||||
if total := len(auth.Providers); total != expected {
|
||||
t.Fatalf("Expected %d providers, got %d", expected, total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProviderByName(t *testing.T) {
|
||||
var err error
|
||||
var p auth.Provider
|
||||
|
||||
// invalid
|
||||
p, err = auth.NewProviderByName("invalid")
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
if p != nil {
|
||||
t.Errorf("Expected provider to be nil, got %v", p)
|
||||
}
|
||||
|
||||
// google
|
||||
p, err = auth.NewProviderByName(auth.NameGoogle)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Google); !ok {
|
||||
t.Error("Expected to be instance of *auth.Google")
|
||||
}
|
||||
|
||||
// facebook
|
||||
p, err = auth.NewProviderByName(auth.NameFacebook)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Facebook); !ok {
|
||||
t.Error("Expected to be instance of *auth.Facebook")
|
||||
}
|
||||
|
||||
// github
|
||||
p, err = auth.NewProviderByName(auth.NameGithub)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Github); !ok {
|
||||
t.Error("Expected to be instance of *auth.Github")
|
||||
}
|
||||
|
||||
// gitlab
|
||||
p, err = auth.NewProviderByName(auth.NameGitlab)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Gitlab); !ok {
|
||||
t.Error("Expected to be instance of *auth.Gitlab")
|
||||
}
|
||||
|
||||
// twitter
|
||||
p, err = auth.NewProviderByName(auth.NameTwitter)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Twitter); !ok {
|
||||
t.Error("Expected to be instance of *auth.Twitter")
|
||||
}
|
||||
|
||||
// discord
|
||||
p, err = auth.NewProviderByName(auth.NameDiscord)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Discord); !ok {
|
||||
t.Error("Expected to be instance of *auth.Discord")
|
||||
}
|
||||
|
||||
// microsoft
|
||||
p, err = auth.NewProviderByName(auth.NameMicrosoft)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Microsoft); !ok {
|
||||
t.Error("Expected to be instance of *auth.Microsoft")
|
||||
}
|
||||
|
||||
// spotify
|
||||
p, err = auth.NewProviderByName(auth.NameSpotify)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Spotify); !ok {
|
||||
t.Error("Expected to be instance of *auth.Spotify")
|
||||
}
|
||||
|
||||
// kakao
|
||||
p, err = auth.NewProviderByName(auth.NameKakao)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Kakao); !ok {
|
||||
t.Error("Expected to be instance of *auth.Kakao")
|
||||
}
|
||||
|
||||
// twitch
|
||||
p, err = auth.NewProviderByName(auth.NameTwitch)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Twitch); !ok {
|
||||
t.Error("Expected to be instance of *auth.Twitch")
|
||||
}
|
||||
|
||||
// strava
|
||||
p, err = auth.NewProviderByName(auth.NameStrava)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Strava); !ok {
|
||||
t.Error("Expected to be instance of *auth.Strava")
|
||||
}
|
||||
|
||||
// gitee
|
||||
p, err = auth.NewProviderByName(auth.NameGitee)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Gitee); !ok {
|
||||
t.Error("Expected to be instance of *auth.Gitee")
|
||||
}
|
||||
|
||||
// livechat
|
||||
p, err = auth.NewProviderByName(auth.NameLivechat)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Livechat); !ok {
|
||||
t.Error("Expected to be instance of *auth.Livechat")
|
||||
}
|
||||
|
||||
// gitea
|
||||
p, err = auth.NewProviderByName(auth.NameGitea)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Gitea); !ok {
|
||||
t.Error("Expected to be instance of *auth.Gitea")
|
||||
}
|
||||
|
||||
// oidc
|
||||
p, err = auth.NewProviderByName(auth.NameOIDC)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.OIDC); !ok {
|
||||
t.Error("Expected to be instance of *auth.OIDC")
|
||||
}
|
||||
|
||||
// oidc2
|
||||
p, err = auth.NewProviderByName(auth.NameOIDC + "2")
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.OIDC); !ok {
|
||||
t.Error("Expected to be instance of *auth.OIDC")
|
||||
}
|
||||
|
||||
// oidc3
|
||||
p, err = auth.NewProviderByName(auth.NameOIDC + "3")
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.OIDC); !ok {
|
||||
t.Error("Expected to be instance of *auth.OIDC")
|
||||
}
|
||||
|
||||
// apple
|
||||
p, err = auth.NewProviderByName(auth.NameApple)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Apple); !ok {
|
||||
t.Error("Expected to be instance of *auth.Apple")
|
||||
}
|
||||
|
||||
// instagram
|
||||
p, err = auth.NewProviderByName(auth.NameInstagram)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Instagram); !ok {
|
||||
t.Error("Expected to be instance of *auth.Instagram")
|
||||
}
|
||||
|
||||
// vk
|
||||
p, err = auth.NewProviderByName(auth.NameVK)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.VK); !ok {
|
||||
t.Error("Expected to be instance of *auth.VK")
|
||||
}
|
||||
|
||||
// yandex
|
||||
p, err = auth.NewProviderByName(auth.NameYandex)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Yandex); !ok {
|
||||
t.Error("Expected to be instance of *auth.Yandex")
|
||||
}
|
||||
|
||||
// patreon
|
||||
p, err = auth.NewProviderByName(auth.NamePatreon)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Patreon); !ok {
|
||||
t.Error("Expected to be instance of *auth.Patreon")
|
||||
}
|
||||
|
||||
// mailcow
|
||||
p, err = auth.NewProviderByName(auth.NameMailcow)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Mailcow); !ok {
|
||||
t.Error("Expected to be instance of *auth.Mailcow")
|
||||
}
|
||||
|
||||
// bitbucket
|
||||
p, err = auth.NewProviderByName(auth.NameBitbucket)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Bitbucket); !ok {
|
||||
t.Error("Expected to be instance of *auth.Bitbucket")
|
||||
}
|
||||
|
||||
// planningcenter
|
||||
p, err = auth.NewProviderByName(auth.NamePlanningcenter)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Planningcenter); !ok {
|
||||
t.Error("Expected to be instance of *auth.Planningcenter")
|
||||
}
|
||||
|
||||
// notion
|
||||
p, err = auth.NewProviderByName(auth.NameNotion)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Notion); !ok {
|
||||
t.Error("Expected to be instance of *auth.Notion")
|
||||
}
|
||||
|
||||
// monday
|
||||
p, err = auth.NewProviderByName(auth.NameMonday)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Monday); !ok {
|
||||
t.Error("Expected to be instance of *auth.Monday")
|
||||
}
|
||||
|
||||
// wakatime
|
||||
p, err = auth.NewProviderByName(auth.NameWakatime)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Wakatime); !ok {
|
||||
t.Error("Expected to be instance of *auth.Wakatime")
|
||||
}
|
||||
|
||||
// linear
|
||||
p, err = auth.NewProviderByName(auth.NameLinear)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Linear); !ok {
|
||||
t.Error("Expected to be instance of *auth.Linear")
|
||||
}
|
||||
|
||||
// trakt
|
||||
p, err = auth.NewProviderByName(auth.NameTrakt)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil, got error %v", err)
|
||||
}
|
||||
if _, ok := p.(*auth.Trakt); !ok {
|
||||
t.Error("Expected to be instance of *auth.Trakt")
|
||||
}
|
||||
}
|
203
tools/auth/base_provider.go
Normal file
203
tools/auth/base_provider.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// BaseProvider defines common fields and methods used by OAuth2 client providers.
|
||||
type BaseProvider struct {
|
||||
ctx context.Context
|
||||
clientId string
|
||||
clientSecret string
|
||||
displayName string
|
||||
redirectURL string
|
||||
authURL string
|
||||
tokenURL string
|
||||
userInfoURL string
|
||||
scopes []string
|
||||
pkce bool
|
||||
extra map[string]any
|
||||
}
|
||||
|
||||
// Context implements Provider.Context() interface method.
|
||||
func (p *BaseProvider) Context() context.Context {
|
||||
return p.ctx
|
||||
}
|
||||
|
||||
// SetContext implements Provider.SetContext() interface method.
|
||||
func (p *BaseProvider) SetContext(ctx context.Context) {
|
||||
p.ctx = ctx
|
||||
}
|
||||
|
||||
// PKCE implements Provider.PKCE() interface method.
|
||||
func (p *BaseProvider) PKCE() bool {
|
||||
return p.pkce
|
||||
}
|
||||
|
||||
// SetPKCE implements Provider.SetPKCE() interface method.
|
||||
func (p *BaseProvider) SetPKCE(enable bool) {
|
||||
p.pkce = enable
|
||||
}
|
||||
|
||||
// DisplayName implements Provider.DisplayName() interface method.
|
||||
func (p *BaseProvider) DisplayName() string {
|
||||
return p.displayName
|
||||
}
|
||||
|
||||
// SetDisplayName implements Provider.SetDisplayName() interface method.
|
||||
func (p *BaseProvider) SetDisplayName(displayName string) {
|
||||
p.displayName = displayName
|
||||
}
|
||||
|
||||
// Scopes implements Provider.Scopes() interface method.
|
||||
func (p *BaseProvider) Scopes() []string {
|
||||
return p.scopes
|
||||
}
|
||||
|
||||
// SetScopes implements Provider.SetScopes() interface method.
|
||||
func (p *BaseProvider) SetScopes(scopes []string) {
|
||||
p.scopes = scopes
|
||||
}
|
||||
|
||||
// ClientId implements Provider.ClientId() interface method.
|
||||
func (p *BaseProvider) ClientId() string {
|
||||
return p.clientId
|
||||
}
|
||||
|
||||
// SetClientId implements Provider.SetClientId() interface method.
|
||||
func (p *BaseProvider) SetClientId(clientId string) {
|
||||
p.clientId = clientId
|
||||
}
|
||||
|
||||
// ClientSecret implements Provider.ClientSecret() interface method.
|
||||
func (p *BaseProvider) ClientSecret() string {
|
||||
return p.clientSecret
|
||||
}
|
||||
|
||||
// SetClientSecret implements Provider.SetClientSecret() interface method.
|
||||
func (p *BaseProvider) SetClientSecret(secret string) {
|
||||
p.clientSecret = secret
|
||||
}
|
||||
|
||||
// RedirectURL implements Provider.RedirectURL() interface method.
|
||||
func (p *BaseProvider) RedirectURL() string {
|
||||
return p.redirectURL
|
||||
}
|
||||
|
||||
// SetRedirectURL implements Provider.SetRedirectURL() interface method.
|
||||
func (p *BaseProvider) SetRedirectURL(url string) {
|
||||
p.redirectURL = url
|
||||
}
|
||||
|
||||
// AuthURL implements Provider.AuthURL() interface method.
|
||||
func (p *BaseProvider) AuthURL() string {
|
||||
return p.authURL
|
||||
}
|
||||
|
||||
// SetAuthURL implements Provider.SetAuthURL() interface method.
|
||||
func (p *BaseProvider) SetAuthURL(url string) {
|
||||
p.authURL = url
|
||||
}
|
||||
|
||||
// TokenURL implements Provider.TokenURL() interface method.
|
||||
func (p *BaseProvider) TokenURL() string {
|
||||
return p.tokenURL
|
||||
}
|
||||
|
||||
// SetTokenURL implements Provider.SetTokenURL() interface method.
|
||||
func (p *BaseProvider) SetTokenURL(url string) {
|
||||
p.tokenURL = url
|
||||
}
|
||||
|
||||
// UserInfoURL implements Provider.UserInfoURL() interface method.
|
||||
func (p *BaseProvider) UserInfoURL() string {
|
||||
return p.userInfoURL
|
||||
}
|
||||
|
||||
// SetUserInfoURL implements Provider.SetUserInfoURL() interface method.
|
||||
func (p *BaseProvider) SetUserInfoURL(url string) {
|
||||
p.userInfoURL = url
|
||||
}
|
||||
|
||||
// Extra implements Provider.Extra() interface method.
|
||||
func (p *BaseProvider) Extra() map[string]any {
|
||||
return maps.Clone(p.extra)
|
||||
}
|
||||
|
||||
// SetExtra implements Provider.SetExtra() interface method.
|
||||
func (p *BaseProvider) SetExtra(data map[string]any) {
|
||||
p.extra = data
|
||||
}
|
||||
|
||||
// BuildAuthURL implements Provider.BuildAuthURL() interface method.
|
||||
func (p *BaseProvider) BuildAuthURL(state string, opts ...oauth2.AuthCodeOption) string {
|
||||
return p.oauth2Config().AuthCodeURL(state, opts...)
|
||||
}
|
||||
|
||||
// FetchToken implements Provider.FetchToken() interface method.
|
||||
func (p *BaseProvider) FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
|
||||
return p.oauth2Config().Exchange(p.ctx, code, opts...)
|
||||
}
|
||||
|
||||
// Client implements Provider.Client() interface method.
|
||||
func (p *BaseProvider) Client(token *oauth2.Token) *http.Client {
|
||||
return p.oauth2Config().Client(p.ctx, token)
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo() interface method.
|
||||
func (p *BaseProvider) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(p.ctx, "GET", p.userInfoURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.sendRawUserInfoRequest(req, token)
|
||||
}
|
||||
|
||||
// sendRawUserInfoRequest sends the specified user info request and return its raw response body.
|
||||
func (p *BaseProvider) sendRawUserInfoRequest(req *http.Request, token *oauth2.Token) ([]byte, error) {
|
||||
client := p.Client(token)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
result, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// http.Client.Get doesn't treat non 2xx responses as error
|
||||
if res.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to fetch OAuth2 user profile via %s (%d):\n%s",
|
||||
p.userInfoURL,
|
||||
res.StatusCode,
|
||||
string(result),
|
||||
)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// oauth2Config constructs a oauth2.Config instance based on the provider settings.
|
||||
func (p *BaseProvider) oauth2Config() *oauth2.Config {
|
||||
return &oauth2.Config{
|
||||
RedirectURL: p.redirectURL,
|
||||
ClientID: p.clientId,
|
||||
ClientSecret: p.clientSecret,
|
||||
Scopes: p.scopes,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: p.authURL,
|
||||
TokenURL: p.tokenURL,
|
||||
},
|
||||
}
|
||||
}
|
269
tools/auth/base_provider_test.go
Normal file
269
tools/auth/base_provider_test.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func TestContext(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.Scopes()
|
||||
if before != nil {
|
||||
t.Errorf("Expected nil context, got %v", before)
|
||||
}
|
||||
|
||||
b.SetContext(context.Background())
|
||||
|
||||
after := b.Scopes()
|
||||
if after != nil {
|
||||
t.Error("Expected non-nil context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayName(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.DisplayName()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected displayName to be empty, got %v", before)
|
||||
}
|
||||
|
||||
b.SetDisplayName("test")
|
||||
|
||||
after := b.DisplayName()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected displayName to be 'test', got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPKCE(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.PKCE()
|
||||
if before != false {
|
||||
t.Fatalf("Expected pkce to be %v, got %v", false, before)
|
||||
}
|
||||
|
||||
b.SetPKCE(true)
|
||||
|
||||
after := b.PKCE()
|
||||
if after != true {
|
||||
t.Fatalf("Expected pkce to be %v, got %v", true, after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopes(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.Scopes()
|
||||
if len(before) != 0 {
|
||||
t.Fatalf("Expected 0 scopes, got %v", before)
|
||||
}
|
||||
|
||||
b.SetScopes([]string{"test1", "test2"})
|
||||
|
||||
after := b.Scopes()
|
||||
if len(after) != 2 {
|
||||
t.Fatalf("Expected 2 scopes, got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientId(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.ClientId()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected clientId to be empty, got %v", before)
|
||||
}
|
||||
|
||||
b.SetClientId("test")
|
||||
|
||||
after := b.ClientId()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected clientId to be 'test', got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSecret(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.ClientSecret()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected clientSecret to be empty, got %v", before)
|
||||
}
|
||||
|
||||
b.SetClientSecret("test")
|
||||
|
||||
after := b.ClientSecret()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected clientSecret to be 'test', got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectURL(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.RedirectURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected RedirectURL to be empty, got %v", before)
|
||||
}
|
||||
|
||||
b.SetRedirectURL("test")
|
||||
|
||||
after := b.RedirectURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected RedirectURL to be 'test', got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthURL(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.AuthURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected authURL to be empty, got %v", before)
|
||||
}
|
||||
|
||||
b.SetAuthURL("test")
|
||||
|
||||
after := b.AuthURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected authURL to be 'test', got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenURL(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.TokenURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected tokenURL to be empty, got %v", before)
|
||||
}
|
||||
|
||||
b.SetTokenURL("test")
|
||||
|
||||
after := b.TokenURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected tokenURL to be 'test', got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserInfoURL(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.UserInfoURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected userInfoURL to be empty, got %v", before)
|
||||
}
|
||||
|
||||
b.SetUserInfoURL("test")
|
||||
|
||||
after := b.UserInfoURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected userInfoURL to be 'test', got %v", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtra(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.Extra()
|
||||
if before != nil {
|
||||
t.Fatalf("Expected extra to be empty, got %v", before)
|
||||
}
|
||||
|
||||
extra := map[string]any{"a": 1, "b": 2}
|
||||
|
||||
b.SetExtra(extra)
|
||||
|
||||
after := b.Extra()
|
||||
|
||||
rawExtra, err := json.Marshal(extra)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rawAfter, err := json.Marshal(after)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(rawExtra, rawAfter) {
|
||||
t.Fatalf("Expected extra to be\n%s\ngot\n%s", rawExtra, rawAfter)
|
||||
}
|
||||
|
||||
// ensure that it was shallow copied
|
||||
after["b"] = 3
|
||||
if d := b.Extra(); d["b"] != 2 {
|
||||
t.Fatalf("Expected extra to remain unchanged, got\n%v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthURL(t *testing.T) {
|
||||
b := BaseProvider{
|
||||
authURL: "authURL_test",
|
||||
tokenURL: "tokenURL_test",
|
||||
redirectURL: "redirectURL_test",
|
||||
clientId: "clientId_test",
|
||||
clientSecret: "clientSecret_test",
|
||||
scopes: []string{"test_scope"},
|
||||
}
|
||||
|
||||
expected := "authURL_test?access_type=offline&client_id=clientId_test&prompt=consent&redirect_uri=redirectURL_test&response_type=code&scope=test_scope&state=state_test"
|
||||
result := b.BuildAuthURL("state_test", oauth2.AccessTypeOffline, oauth2.ApprovalForce)
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected auth url %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
result := b.Client(&oauth2.Token{})
|
||||
if result == nil {
|
||||
t.Error("Expected *http.Client instance, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2Config(t *testing.T) {
|
||||
b := BaseProvider{
|
||||
authURL: "authURL_test",
|
||||
tokenURL: "tokenURL_test",
|
||||
redirectURL: "redirectURL_test",
|
||||
clientId: "clientId_test",
|
||||
clientSecret: "clientSecret_test",
|
||||
scopes: []string{"test"},
|
||||
}
|
||||
|
||||
result := b.oauth2Config()
|
||||
|
||||
if result.RedirectURL != b.RedirectURL() {
|
||||
t.Errorf("Expected redirectURL %s, got %s", b.RedirectURL(), result.RedirectURL)
|
||||
}
|
||||
|
||||
if result.ClientID != b.ClientId() {
|
||||
t.Errorf("Expected clientId %s, got %s", b.ClientId(), result.ClientID)
|
||||
}
|
||||
|
||||
if result.ClientSecret != b.ClientSecret() {
|
||||
t.Errorf("Expected clientSecret %s, got %s", b.ClientSecret(), result.ClientSecret)
|
||||
}
|
||||
|
||||
if result.Endpoint.AuthURL != b.AuthURL() {
|
||||
t.Errorf("Expected authURL %s, got %s", b.AuthURL(), result.Endpoint.AuthURL)
|
||||
}
|
||||
|
||||
if result.Endpoint.TokenURL != b.TokenURL() {
|
||||
t.Errorf("Expected authURL %s, got %s", b.TokenURL(), result.Endpoint.TokenURL)
|
||||
}
|
||||
|
||||
if len(result.Scopes) != len(b.Scopes()) || result.Scopes[0] != b.Scopes()[0] {
|
||||
t.Errorf("Expected scopes %s, got %s", b.Scopes(), result.Scopes)
|
||||
}
|
||||
}
|
136
tools/auth/bitbucket.go
Normal file
136
tools/auth/bitbucket.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameBitbucket] = wrapFactory(NewBitbucketProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Bitbucket)(nil)
|
||||
|
||||
// NameBitbucket is the unique name of the Bitbucket provider.
|
||||
const NameBitbucket = "bitbucket"
|
||||
|
||||
// Bitbucket is an auth provider for Bitbucket.
|
||||
type Bitbucket struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewBitbucketProvider creates a new Bitbucket provider instance with some defaults.
|
||||
func NewBitbucketProvider() *Bitbucket {
|
||||
return &Bitbucket{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Bitbucket",
|
||||
pkce: false,
|
||||
scopes: []string{"account"},
|
||||
authURL: "https://bitbucket.org/site/oauth2/authorize",
|
||||
tokenURL: "https://bitbucket.org/site/oauth2/access_token",
|
||||
userInfoURL: "https://api.bitbucket.org/2.0/user",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Bitbucket's user API.
|
||||
//
|
||||
// API reference: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-get
|
||||
func (p *Bitbucket) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
UUID string `json:"uuid"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
AccountStatus string `json:"account_status"`
|
||||
Links struct {
|
||||
Avatar struct {
|
||||
Href string `json:"href"`
|
||||
} `json:"avatar"`
|
||||
} `json:"links"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if extracted.AccountStatus != "active" {
|
||||
return nil, errors.New("the Bitbucket user is not active")
|
||||
}
|
||||
|
||||
email, err := p.fetchPrimaryEmail(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.UUID,
|
||||
Name: extracted.DisplayName,
|
||||
Username: extracted.Username,
|
||||
Email: email,
|
||||
AvatarURL: extracted.Links.Avatar.Href,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// fetchPrimaryEmail sends an API request to retrieve the first
|
||||
// verified primary email.
|
||||
//
|
||||
// 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://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-emails-get
|
||||
func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
response, err := p.Client(token).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 >= 400 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
expected := struct {
|
||||
Values []struct {
|
||||
Email string `json:"email"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
} `json:"values"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &expected); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, v := range expected.Values {
|
||||
if v.IsPrimary {
|
||||
return v.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
91
tools/auth/discord.go
Normal file
91
tools/auth/discord.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameDiscord] = wrapFactory(NewDiscordProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Discord)(nil)
|
||||
|
||||
// NameDiscord is the unique name of the Discord provider.
|
||||
const NameDiscord string = "discord"
|
||||
|
||||
// Discord allows authentication via Discord OAuth2.
|
||||
type Discord struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewDiscordProvider creates a new Discord provider instance with some defaults.
|
||||
func NewDiscordProvider() *Discord {
|
||||
// https://discord.com/developers/docs/topics/oauth2
|
||||
// https://discord.com/developers/docs/resources/user#get-current-user
|
||||
return &Discord{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Discord",
|
||||
pkce: true,
|
||||
scopes: []string{"identify", "email"},
|
||||
authURL: "https://discord.com/api/oauth2/authorize",
|
||||
tokenURL: "https://discord.com/api/oauth2/token",
|
||||
userInfoURL: "https://discord.com/api/users/@me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance from Discord's user api.
|
||||
//
|
||||
// API reference: https://discord.com/developers/docs/resources/user#user-object
|
||||
func (p *Discord) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Discriminator string `json:"discriminator"`
|
||||
Avatar string `json:"avatar"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"verified"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build a full avatar URL using the avatar hash provided in the API response
|
||||
// https://discord.com/developers/docs/reference#image-formatting
|
||||
avatarURL := fmt.Sprintf("https://cdn.discordapp.com/avatars/%s/%s.png", extracted.Id, extracted.Avatar)
|
||||
|
||||
// Concatenate the user's username and discriminator into a single username string
|
||||
username := fmt.Sprintf("%s#%s", extracted.Username, extracted.Discriminator)
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: username,
|
||||
Username: extracted.Username,
|
||||
AvatarURL: avatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if extracted.Verified {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
78
tools/auth/facebook.go
Normal file
78
tools/auth/facebook.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/facebook"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameFacebook] = wrapFactory(NewFacebookProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Facebook)(nil)
|
||||
|
||||
// NameFacebook is the unique name of the Facebook provider.
|
||||
const NameFacebook string = "facebook"
|
||||
|
||||
// Facebook allows authentication via Facebook OAuth2.
|
||||
type Facebook struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewFacebookProvider creates new Facebook provider instance with some defaults.
|
||||
func NewFacebookProvider() *Facebook {
|
||||
return &Facebook{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Facebook",
|
||||
pkce: true,
|
||||
scopes: []string{"email"},
|
||||
authURL: facebook.Endpoint.AuthURL,
|
||||
tokenURL: facebook.Endpoint.TokenURL,
|
||||
userInfoURL: "https://graph.facebook.com/me?fields=name,email,picture.type(large)",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Facebook's user api.
|
||||
//
|
||||
// API reference: https://developers.facebook.com/docs/graph-api/reference/user/
|
||||
func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string
|
||||
Name string
|
||||
Email string
|
||||
Picture struct {
|
||||
Data struct{ Url string }
|
||||
}
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
AvatarURL: extracted.Picture.Data.Url,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
78
tools/auth/gitea.go
Normal file
78
tools/auth/gitea.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameGitea] = wrapFactory(NewGiteaProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Gitea)(nil)
|
||||
|
||||
// NameGitea is the unique name of the Gitea provider.
|
||||
const NameGitea string = "gitea"
|
||||
|
||||
// Gitea allows authentication via Gitea OAuth2.
|
||||
type Gitea struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewGiteaProvider creates new Gitea provider instance with some defaults.
|
||||
func NewGiteaProvider() *Gitea {
|
||||
return &Gitea{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Gitea",
|
||||
pkce: true,
|
||||
scopes: []string{"read:user", "user:email"},
|
||||
authURL: "https://gitea.com/login/oauth/authorize",
|
||||
tokenURL: "https://gitea.com/login/oauth/access_token",
|
||||
userInfoURL: "https://gitea.com/api/v1/user",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on Gitea's user api.
|
||||
//
|
||||
// API reference: https://try.gitea.io/api/swagger#/user/userGetCurrent
|
||||
func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Name string `json:"full_name"`
|
||||
Username string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id int64 `json:"id"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
142
tools/auth/gitee.go
Normal file
142
tools/auth/gitee.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameGitee] = wrapFactory(NewGiteeProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Gitee)(nil)
|
||||
|
||||
// NameGitee is the unique name of the Gitee provider.
|
||||
const NameGitee string = "gitee"
|
||||
|
||||
// Gitee allows authentication via Gitee OAuth2.
|
||||
type Gitee struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewGiteeProvider creates new Gitee provider instance with some defaults.
|
||||
func NewGiteeProvider() *Gitee {
|
||||
return &Gitee{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Gitee",
|
||||
pkce: true,
|
||||
scopes: []string{"user_info", "emails"},
|
||||
authURL: "https://gitee.com/oauth/authorize",
|
||||
tokenURL: "https://gitee.com/oauth/token",
|
||||
userInfoURL: "https://gitee.com/api/v5/user",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the Gitee's user api.
|
||||
//
|
||||
// API reference: https://gitee.com/api/v5/swagger#/getV5User
|
||||
func (p *Gitee) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id int64 `json:"id"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Login,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if extracted.Email != "" && is.EmailFormat.Validate(extracted.Email) == nil {
|
||||
// valid public primary email
|
||||
user.Email = extracted.Email
|
||||
} else {
|
||||
// send an additional optional request to retrieve the email
|
||||
email, err := p.fetchPrimaryEmail(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Email = email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// fetchPrimaryEmail sends an API request to retrieve the verified primary email,
|
||||
// in case the user hasn't set "Public email address" or has unchecked
|
||||
// the "Access your emails data" permission during authentication.
|
||||
//
|
||||
// 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://gitee.com/api/v5/swagger#/getV5Emails
|
||||
func (p *Gitee) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
client := p.Client(token)
|
||||
|
||||
response, err := client.Get("https://gitee.com/api/v5/emails")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// ignore common http errors caused by insufficient scope permissions
|
||||
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
|
||||
State string
|
||||
Scope []string
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &emails); err != nil {
|
||||
// ignore unmarshal error in case "Keep my email address private"
|
||||
// was set because response.Body will be something like:
|
||||
// {"email":"12285415+test@user.noreply.gitee.com"}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// extract the first verified primary email
|
||||
for _, email := range emails {
|
||||
for _, scope := range email.Scope {
|
||||
if email.State == "confirmed" && scope == "primary" && is.EmailFormat.Validate(email.Email) == nil {
|
||||
return email.Email, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
136
tools/auth/github.go
Normal file
136
tools/auth/github.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/github"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameGithub] = wrapFactory(NewGithubProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Github)(nil)
|
||||
|
||||
// NameGithub is the unique name of the Github provider.
|
||||
const NameGithub string = "github"
|
||||
|
||||
// Github allows authentication via Github OAuth2.
|
||||
type Github struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewGithubProvider creates new Github provider instance with some defaults.
|
||||
func NewGithubProvider() *Github {
|
||||
return &Github{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "GitHub",
|
||||
pkce: true, // technically is not supported yet but it is safe as the PKCE params are just ignored
|
||||
scopes: []string{"read:user", "user:email"},
|
||||
authURL: github.Endpoint.AuthURL,
|
||||
tokenURL: github.Endpoint.TokenURL,
|
||||
userInfoURL: "https://api.github.com/user",
|
||||
}}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id int64 `json:"id"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Login,
|
||||
Email: extracted.Email,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// fetchPrimaryEmail 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://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28
|
||||
func (p *Github) fetchPrimaryEmail(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
|
||||
}
|
78
tools/auth/gitlab.go
Normal file
78
tools/auth/gitlab.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameGitlab] = wrapFactory(NewGitlabProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Gitlab)(nil)
|
||||
|
||||
// NameGitlab is the unique name of the Gitlab provider.
|
||||
const NameGitlab string = "gitlab"
|
||||
|
||||
// Gitlab allows authentication via Gitlab OAuth2.
|
||||
type Gitlab struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewGitlabProvider creates new Gitlab provider instance with some defaults.
|
||||
func NewGitlabProvider() *Gitlab {
|
||||
return &Gitlab{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "GitLab",
|
||||
pkce: true,
|
||||
scopes: []string{"read_user"},
|
||||
authURL: "https://gitlab.com/oauth/authorize",
|
||||
tokenURL: "https://gitlab.com/oauth/token",
|
||||
userInfoURL: "https://gitlab.com/api/v4/user",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the Gitlab's user api.
|
||||
//
|
||||
// API reference: https://docs.gitlab.com/ee/api/users.html#for-admin
|
||||
func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id int64 `json:"id"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
80
tools/auth/google.go
Normal file
80
tools/auth/google.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameGoogle] = wrapFactory(NewGoogleProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Google)(nil)
|
||||
|
||||
// NameGoogle is the unique name of the Google provider.
|
||||
const NameGoogle string = "google"
|
||||
|
||||
// Google allows authentication via Google OAuth2.
|
||||
type Google struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewGoogleProvider creates new Google provider instance with some defaults.
|
||||
func NewGoogleProvider() *Google {
|
||||
return &Google{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Google",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
},
|
||||
authURL: "https://accounts.google.com/o/oauth2/v2/auth",
|
||||
tokenURL: "https://oauth2.googleapis.com/token",
|
||||
userInfoURL: "https://www.googleapis.com/oauth2/v3/userinfo",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the Google's user api.
|
||||
func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Picture string `json:"picture"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
AvatarURL: extracted.Picture,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if extracted.EmailVerified {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
82
tools/auth/instagram.go
Normal file
82
tools/auth/instagram.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameInstagram] = wrapFactory(NewInstagramProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Instagram)(nil)
|
||||
|
||||
// NameInstagram is the unique name of the Instagram provider.
|
||||
const NameInstagram string = "instagram2" // "2" suffix to avoid conflicts with the old deprecated version
|
||||
|
||||
// Instagram allows authentication via Instagram Login OAuth2.
|
||||
type Instagram struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewInstagramProvider creates new Instagram provider instance with some defaults.
|
||||
func NewInstagramProvider() *Instagram {
|
||||
return &Instagram{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Instagram",
|
||||
pkce: true,
|
||||
scopes: []string{"instagram_business_basic"},
|
||||
authURL: "https://www.instagram.com/oauth/authorize",
|
||||
tokenURL: "https://api.instagram.com/oauth/access_token",
|
||||
userInfoURL: "https://graph.instagram.com/me?fields=id,username,account_type,user_id,name,profile_picture_url,followers_count,follows_count,media_count",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Instagram Login user api response.
|
||||
//
|
||||
// API reference: https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started#fields
|
||||
func (p *Instagram) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// include list of granted permissions to RawUser's payload
|
||||
if _, ok := rawUser["permissions"]; !ok {
|
||||
if permissions := token.Extra("permissions"); permissions != nil {
|
||||
rawUser["permissions"] = permissions
|
||||
}
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"profile_picture_url"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Username: extracted.Username,
|
||||
Name: extracted.Name,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
86
tools/auth/kakao.go
Normal file
86
tools/auth/kakao.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/kakao"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameKakao] = wrapFactory(NewKakaoProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Kakao)(nil)
|
||||
|
||||
// NameKakao is the unique name of the Kakao provider.
|
||||
const NameKakao string = "kakao"
|
||||
|
||||
// Kakao allows authentication via Kakao OAuth2.
|
||||
type Kakao struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewKakaoProvider creates a new Kakao provider instance with some defaults.
|
||||
func NewKakaoProvider() *Kakao {
|
||||
return &Kakao{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Kakao",
|
||||
pkce: true,
|
||||
scopes: []string{"account_email", "profile_nickname", "profile_image"},
|
||||
authURL: kakao.Endpoint.AuthURL,
|
||||
tokenURL: kakao.Endpoint.TokenURL,
|
||||
userInfoURL: "https://kapi.kakao.com/v2/user/me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Kakao's user api.
|
||||
//
|
||||
// API reference: https://developers.kakao.com/docs/latest/en/kakaologin/rest-api#req-user-info-response
|
||||
func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Profile struct {
|
||||
Nickname string `json:"nickname"`
|
||||
ImageURL string `json:"profile_image"`
|
||||
} `json:"properties"`
|
||||
KakaoAccount struct {
|
||||
Email string `json:"email"`
|
||||
IsEmailVerified bool `json:"is_email_verified"`
|
||||
IsEmailValid bool `json:"is_email_valid"`
|
||||
} `json:"kakao_account"`
|
||||
Id int64 `json:"id"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Username: extracted.Profile.Nickname,
|
||||
AvatarURL: extracted.Profile.ImageURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if extracted.KakaoAccount.IsEmailValid && extracted.KakaoAccount.IsEmailVerified {
|
||||
user.Email = extracted.KakaoAccount.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
109
tools/auth/linear.go
Normal file
109
tools/auth/linear.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameLinear] = wrapFactory(NewLinearProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Linear)(nil)
|
||||
|
||||
// NameLinear is the unique name of the Linear provider.
|
||||
const NameLinear string = "linear"
|
||||
|
||||
// Linear allows authentication via Linear OAuth2.
|
||||
type Linear struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewLinearProvider creates new Linear provider instance with some defaults.
|
||||
//
|
||||
// API reference: https://developers.linear.app/docs/oauth/authentication
|
||||
func NewLinearProvider() *Linear {
|
||||
return &Linear{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Linear",
|
||||
pkce: false, // Linear doesn't support PKCE at the moment and returns an error if enabled
|
||||
scopes: []string{"read"},
|
||||
authURL: "https://linear.app/oauth/authorize",
|
||||
tokenURL: "https://api.linear.app/oauth/token",
|
||||
userInfoURL: "https://api.linear.app/graphql",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Linear's user api.
|
||||
//
|
||||
// API reference: https://developers.linear.app/docs/graphql/working-with-the-graphql-api#authentication
|
||||
func (p *Linear) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Data struct {
|
||||
Viewer struct {
|
||||
Id string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatarUrl"`
|
||||
Active bool `json:"active"`
|
||||
} `json:"viewer"`
|
||||
} `json:"data"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !extracted.Data.Viewer.Active {
|
||||
return nil, errors.New("the Linear user account is not active")
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data.Viewer.Id,
|
||||
Name: extracted.Data.Viewer.Name,
|
||||
Username: extracted.Data.Viewer.DisplayName,
|
||||
Email: extracted.Data.Viewer.Email,
|
||||
AvatarURL: extracted.Data.Viewer.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
|
||||
//
|
||||
// Linear doesn't have a UserInfo endpoint and information on the user
|
||||
// is retrieved using their GraphQL API (https://developers.linear.app/docs/graphql/working-with-the-graphql-api#queries-and-mutations)
|
||||
func (p *Linear) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
query := []byte(`{"query": "query Me { viewer { id displayName name email avatarUrl active } }"}`)
|
||||
bodyReader := bytes.NewReader(query)
|
||||
|
||||
req, err := http.NewRequestWithContext(p.ctx, "POST", p.userInfoURL, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
return p.sendRawUserInfoRequest(req, token)
|
||||
}
|
79
tools/auth/livechat.go
Normal file
79
tools/auth/livechat.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameLivechat] = wrapFactory(NewLivechatProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Livechat)(nil)
|
||||
|
||||
// NameLivechat is the unique name of the Livechat provider.
|
||||
const NameLivechat = "livechat"
|
||||
|
||||
// Livechat allows authentication via Livechat OAuth2.
|
||||
type Livechat struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewLivechatProvider creates new Livechat provider instance with some defaults.
|
||||
func NewLivechatProvider() *Livechat {
|
||||
return &Livechat{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "LiveChat",
|
||||
pkce: true,
|
||||
scopes: []string{}, // default scopes are specified from the provider dashboard
|
||||
authURL: "https://accounts.livechat.com/",
|
||||
tokenURL: "https://accounts.livechat.com/token",
|
||||
userInfoURL: "https://accounts.livechat.com/v2/accounts/me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser based on the Livechat accounts API.
|
||||
//
|
||||
// API reference: https://developers.livechat.com/docs/authorization
|
||||
func (p *Livechat) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"account_id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if extracted.EmailVerified {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
84
tools/auth/mailcow.go
Normal file
84
tools/auth/mailcow.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameMailcow] = wrapFactory(NewMailcowProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Mailcow)(nil)
|
||||
|
||||
// NameMailcow is the unique name of the mailcow provider.
|
||||
const NameMailcow string = "mailcow"
|
||||
|
||||
// Mailcow allows authentication via mailcow OAuth2.
|
||||
type Mailcow struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewMailcowProvider creates a new mailcow provider instance with some defaults.
|
||||
func NewMailcowProvider() *Mailcow {
|
||||
return &Mailcow{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "mailcow",
|
||||
pkce: true,
|
||||
scopes: []string{"profile"},
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on mailcow's user api.
|
||||
//
|
||||
// API reference: https://github.com/mailcow/mailcow-dockerized/blob/master/data/web/oauth/profile.php
|
||||
func (p *Mailcow) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"full_name"`
|
||||
Active int `json:"active"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if extracted.Active != 1 {
|
||||
return nil, errors.New("the mailcow user is not active")
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.FullName,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
// mailcow usernames are usually just the email adresses, so we just take the part in front of the @
|
||||
if strings.Contains(user.Username, "@") {
|
||||
user.Username = strings.Split(user.Username, "@")[0]
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
76
tools/auth/microsoft.go
Normal file
76
tools/auth/microsoft.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/microsoft"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameMicrosoft] = wrapFactory(NewMicrosoftProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Microsoft)(nil)
|
||||
|
||||
// NameMicrosoft is the unique name of the Microsoft provider.
|
||||
const NameMicrosoft string = "microsoft"
|
||||
|
||||
// Microsoft allows authentication via AzureADEndpoint OAuth2.
|
||||
type Microsoft struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewMicrosoftProvider creates new Microsoft AD provider instance with some defaults.
|
||||
func NewMicrosoftProvider() *Microsoft {
|
||||
endpoints := microsoft.AzureADEndpoint("")
|
||||
return &Microsoft{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Microsoft",
|
||||
pkce: true,
|
||||
scopes: []string{"User.Read"},
|
||||
authURL: endpoints.AuthURL,
|
||||
tokenURL: endpoints.TokenURL,
|
||||
userInfoURL: "https://graph.microsoft.com/v1.0/me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Microsoft's user api.
|
||||
//
|
||||
// API reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo
|
||||
// Graph explorer: https://developer.microsoft.com/en-us/graph/graph-explorer
|
||||
func (p *Microsoft) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"displayName"`
|
||||
Email string `json:"mail"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Email: extracted.Email,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
108
tools/auth/monday.go
Normal file
108
tools/auth/monday.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameMonday] = wrapFactory(NewMondayProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Monday)(nil)
|
||||
|
||||
// NameMonday is the unique name of the Monday provider.
|
||||
const NameMonday = "monday"
|
||||
|
||||
// Monday is an auth provider for monday.com.
|
||||
type Monday struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewMondayProvider creates a new Monday provider instance with some defaults.
|
||||
func NewMondayProvider() *Monday {
|
||||
return &Monday{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "monday.com",
|
||||
pkce: true,
|
||||
scopes: []string{"me:read"},
|
||||
authURL: "https://auth.monday.com/oauth2/authorize",
|
||||
tokenURL: "https://auth.monday.com/oauth2/token",
|
||||
userInfoURL: "https://api.monday.com/v2",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Monday's user api.
|
||||
//
|
||||
// API reference: https://developer.monday.com/api-reference/reference/me
|
||||
func (p *Monday) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Data struct {
|
||||
Me struct {
|
||||
Id string `json:"id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
Avatar string `json:"photo_small"`
|
||||
} `json:"me"`
|
||||
} `json:"data"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !extracted.Data.Me.Enabled {
|
||||
return nil, errors.New("the monday.com user account is not enabled")
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data.Me.Id,
|
||||
Name: extracted.Data.Me.Name,
|
||||
AvatarURL: extracted.Data.Me.Avatar,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
if extracted.Data.Me.IsVerified {
|
||||
user.Email = extracted.Data.Me.Email
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface.
|
||||
//
|
||||
// monday.com doesn't have a UserInfo endpoint and information on the user
|
||||
// is retrieved using their GraphQL API (https://developer.monday.com/api-reference/reference/me#queries)
|
||||
func (p *Monday) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
query := []byte(`{"query": "query { me { id enabled name email is_verified photo_small }}"}`)
|
||||
bodyReader := bytes.NewReader(query)
|
||||
|
||||
req, err := http.NewRequestWithContext(p.ctx, "POST", p.userInfoURL, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
return p.sendRawUserInfoRequest(req, token)
|
||||
}
|
106
tools/auth/notion.go
Normal file
106
tools/auth/notion.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameNotion] = wrapFactory(NewNotionProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Notion)(nil)
|
||||
|
||||
// NameNotion is the unique name of the Notion provider.
|
||||
const NameNotion string = "notion"
|
||||
|
||||
// Notion allows authentication via Notion OAuth2.
|
||||
type Notion struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewNotionProvider creates new Notion provider instance with some defaults.
|
||||
func NewNotionProvider() *Notion {
|
||||
return &Notion{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Notion",
|
||||
pkce: true,
|
||||
authURL: "https://api.notion.com/v1/oauth/authorize",
|
||||
tokenURL: "https://api.notion.com/v1/oauth/token",
|
||||
userInfoURL: "https://api.notion.com/v1/users/me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Notion's User api.
|
||||
// API reference: https://developers.notion.com/reference/get-self
|
||||
func (p *Notion) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Bot struct {
|
||||
Owner struct {
|
||||
Type string `json:"type"`
|
||||
User struct {
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Person struct {
|
||||
Email string `json:"email"`
|
||||
} `json:"person"`
|
||||
} `json:"user"`
|
||||
} `json:"owner"`
|
||||
WorkspaceName string `json:"workspace_name"`
|
||||
} `json:"bot"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Object string `json:"object"`
|
||||
RequestId string `json:"request_id"`
|
||||
Type string `json:"type"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Bot.Owner.User.Id,
|
||||
Name: extracted.Bot.Owner.User.Name,
|
||||
Email: extracted.Bot.Owner.User.Person.Email,
|
||||
AvatarURL: extracted.Bot.Owner.User.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
|
||||
//
|
||||
// This differ from BaseProvider because Notion requires a version header for all requests
|
||||
// (https://developers.notion.com/reference/versioning).
|
||||
func (p *Notion) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(p.ctx, "GET", p.userInfoURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Notion-Version", "2022-06-28")
|
||||
|
||||
return p.sendRawUserInfoRequest(req, token)
|
||||
}
|
292
tools/auth/oidc.go
Normal file
292
tools/auth/oidc.go
Normal file
|
@ -0,0 +1,292 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// idTokenLeeway is the optional leeway for the id_token timestamp fields validation.
|
||||
//
|
||||
// It can be changed externally using the PB_ID_TOKEN_LEEWAY env variable
|
||||
// (the value must be in seconds, e.g. "PB_ID_TOKEN_LEEWAY=60" for 1 minute).
|
||||
var idTokenLeeway time.Duration = 5 * time.Minute
|
||||
|
||||
func init() {
|
||||
Providers[NameOIDC] = wrapFactory(NewOIDCProvider)
|
||||
Providers[NameOIDC+"2"] = wrapFactory(NewOIDCProvider)
|
||||
Providers[NameOIDC+"3"] = wrapFactory(NewOIDCProvider)
|
||||
|
||||
if leewayStr := os.Getenv("PB_ID_TOKEN_LEEWAY"); leewayStr != "" {
|
||||
leeway, err := strconv.Atoi(leewayStr)
|
||||
if err == nil {
|
||||
idTokenLeeway = time.Duration(leeway) * time.Second
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var _ Provider = (*OIDC)(nil)
|
||||
|
||||
// NameOIDC is the unique name of the OpenID Connect (OIDC) provider.
|
||||
const NameOIDC string = "oidc"
|
||||
|
||||
// OIDC allows authentication via OpenID Connect (OIDC) OAuth2 provider.
|
||||
//
|
||||
// If specified the user data is fetched from the userInfoURL.
|
||||
// Otherwise - from the id_token payload.
|
||||
//
|
||||
// The provider support the following Extra config options:
|
||||
// - "jwksURL" - url to the keys to validate the id_token signature (optional and used only when reading the user data from the id_token)
|
||||
// - "issuers" - list of valid issuers for the iss id_token claim (optioanl and used only when reading the user data from the id_token)
|
||||
type OIDC struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewOIDCProvider creates new OpenID Connect (OIDC) provider instance with some defaults.
|
||||
func NewOIDCProvider() *OIDC {
|
||||
return &OIDC{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "OIDC",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
"openid", // minimal requirement to return the id
|
||||
"email",
|
||||
"profile",
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the provider's user api.
|
||||
//
|
||||
// API reference: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
|
||||
func (p *OIDC) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"preferred_username"`
|
||||
Picture string `json:"picture"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified any `json:"email_verified"` // see #6657
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
AvatarURL: extracted.Picture,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if cast.ToBool(extracted.EmailVerified) {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
|
||||
//
|
||||
// It either fetch the data from p.userInfoURL, or if not set - returns the id_token claims.
|
||||
func (p *OIDC) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
if p.userInfoURL != "" {
|
||||
return p.BaseProvider.FetchRawUserInfo(token)
|
||||
}
|
||||
|
||||
claims, err := p.parseIdToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(claims)
|
||||
}
|
||||
|
||||
func (p *OIDC) parseIdToken(token *oauth2.Token) (jwt.MapClaims, error) {
|
||||
idToken := token.Extra("id_token").(string)
|
||||
if idToken == "" {
|
||||
return nil, errors.New("empty id_token")
|
||||
}
|
||||
|
||||
claims := jwt.MapClaims{}
|
||||
t, _, err := jwt.NewParser().ParseUnverified(idToken, claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate common claims
|
||||
jwtValidator := jwt.NewValidator(
|
||||
jwt.WithIssuedAt(),
|
||||
jwt.WithLeeway(idTokenLeeway),
|
||||
jwt.WithAudience(p.clientId),
|
||||
)
|
||||
err = jwtValidator.Validate(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// validate iss (if "issuers" extra config is set)
|
||||
issuers := cast.ToStringSlice(p.Extra()["issuers"])
|
||||
if len(issuers) > 0 {
|
||||
var isIssValid bool
|
||||
claimIssuer, _ := claims.GetIssuer()
|
||||
|
||||
for _, issuer := range issuers {
|
||||
if security.Equal(claimIssuer, issuer) {
|
||||
isIssValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isIssValid {
|
||||
return nil, fmt.Errorf("iss must be one of %v, got %#v", issuers, claims["iss"])
|
||||
}
|
||||
}
|
||||
|
||||
// validate signature (if "jwksURL" extra config is set)
|
||||
//
|
||||
// note: this step could be technically considered optional because we trust
|
||||
// the token which is a result of direct TLS communication with the provider
|
||||
// (see also https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation)
|
||||
jwksURL := cast.ToString(p.Extra()["jwksURL"])
|
||||
if jwksURL != "" {
|
||||
kid, _ := t.Header["kid"].(string)
|
||||
err = validateIdTokenSignature(p.ctx, idToken, jwksURL, kid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func validateIdTokenSignature(ctx context.Context, idToken string, jwksURL string, kid string) error {
|
||||
// fetch the public key set
|
||||
// ---
|
||||
if kid == "" {
|
||||
return errors.New("missing kid header value")
|
||||
}
|
||||
|
||||
key, err := fetchJWK(ctx, jwksURL, kid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode the key params per RFC 7518 (https://tools.ietf.org/html/rfc7518#section-6.3)
|
||||
// and construct a valid publicKey from them
|
||||
// ---
|
||||
exponent, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.E, "="))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
modulus, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(key.N, "="))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publicKey := &rsa.PublicKey{
|
||||
// https://tools.ietf.org/html/rfc7517#appendix-A.1
|
||||
E: int(big.NewInt(0).SetBytes(exponent).Uint64()),
|
||||
N: big.NewInt(0).SetBytes(modulus),
|
||||
}
|
||||
|
||||
// verify the signiture
|
||||
// ---
|
||||
parser := jwt.NewParser(jwt.WithValidMethods([]string{key.Alg}))
|
||||
|
||||
parsedToken, err := parser.Parse(idToken, func(t *jwt.Token) (any, error) {
|
||||
return publicKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !parsedToken.Valid {
|
||||
return errors.New("the parsed id_token is invalid")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type jwk struct {
|
||||
Kty string
|
||||
Kid string
|
||||
Use string
|
||||
Alg string
|
||||
N string
|
||||
E string
|
||||
}
|
||||
|
||||
func fetchJWK(ctx context.Context, jwksURL string, kid string) (*jwk, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", jwksURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
rawBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// http.Client.Get doesn't treat non 2xx responses as error
|
||||
if res.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to verify the provided id_token (%d):\n%s",
|
||||
res.StatusCode,
|
||||
string(rawBody),
|
||||
)
|
||||
}
|
||||
|
||||
jwks := struct {
|
||||
Keys []*jwk
|
||||
}{}
|
||||
if err := json.Unmarshal(rawBody, &jwks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, key := range jwks.Keys {
|
||||
if key.Kid == kid {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("jwk with kid %q was not found", kid)
|
||||
}
|
88
tools/auth/patreon.go
Normal file
88
tools/auth/patreon.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/endpoints"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NamePatreon] = wrapFactory(NewPatreonProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Patreon)(nil)
|
||||
|
||||
// NamePatreon is the unique name of the Patreon provider.
|
||||
const NamePatreon string = "patreon"
|
||||
|
||||
// Patreon allows authentication via Patreon OAuth2.
|
||||
type Patreon struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewPatreonProvider creates new Patreon provider instance with some defaults.
|
||||
func NewPatreonProvider() *Patreon {
|
||||
return &Patreon{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Patreon",
|
||||
pkce: true,
|
||||
scopes: []string{"identity", "identity[email]"},
|
||||
authURL: endpoints.Patreon.AuthURL,
|
||||
tokenURL: endpoints.Patreon.TokenURL,
|
||||
userInfoURL: "https://www.patreon.com/api/oauth2/v2/identity?fields%5Buser%5D=full_name,email,vanity,image_url,is_email_verified",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Patreons's identity api.
|
||||
//
|
||||
// API reference:
|
||||
// https://docs.patreon.com/#get-api-oauth2-v2-identity
|
||||
// https://docs.patreon.com/#user-v2
|
||||
func (p *Patreon) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Data struct {
|
||||
Id string `json:"id"`
|
||||
Attributes struct {
|
||||
Email string `json:"email"`
|
||||
Name string `json:"full_name"`
|
||||
Username string `json:"vanity"`
|
||||
AvatarURL string `json:"image_url"`
|
||||
IsEmailVerified bool `json:"is_email_verified"`
|
||||
} `json:"attributes"`
|
||||
} `json:"data"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data.Id,
|
||||
Username: extracted.Data.Attributes.Username,
|
||||
Name: extracted.Data.Attributes.Name,
|
||||
AvatarURL: extracted.Data.Attributes.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if extracted.Data.Attributes.IsEmailVerified {
|
||||
user.Email = extracted.Data.Attributes.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
85
tools/auth/planningcenter.go
Normal file
85
tools/auth/planningcenter.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NamePlanningcenter] = wrapFactory(NewPlanningcenterProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Planningcenter)(nil)
|
||||
|
||||
// NamePlanningcenter is the unique name of the Planningcenter provider.
|
||||
const NamePlanningcenter string = "planningcenter"
|
||||
|
||||
// Planningcenter allows authentication via Planningcenter OAuth2.
|
||||
type Planningcenter struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewPlanningcenterProvider creates a new Planningcenter provider instance with some defaults.
|
||||
func NewPlanningcenterProvider() *Planningcenter {
|
||||
return &Planningcenter{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Planning Center",
|
||||
pkce: true,
|
||||
scopes: []string{"people"},
|
||||
authURL: "https://api.planningcenteronline.com/oauth/authorize",
|
||||
tokenURL: "https://api.planningcenteronline.com/oauth/token",
|
||||
userInfoURL: "https://api.planningcenteronline.com/people/v2/me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Planningcenter's user api.
|
||||
//
|
||||
// API reference: https://developer.planning.center/docs/#/overview/authentication
|
||||
func (p *Planningcenter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Data struct {
|
||||
Id string `json:"id"`
|
||||
Attributes struct {
|
||||
Status string `json:"status"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar"`
|
||||
// don't map the email because users can have multiple assigned
|
||||
// and it's not clear if they are verified
|
||||
}
|
||||
}
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if extracted.Data.Attributes.Status != "active" {
|
||||
return nil, errors.New("the user is not active")
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data.Id,
|
||||
Name: extracted.Data.Attributes.Name,
|
||||
AvatarURL: extracted.Data.Attributes.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
87
tools/auth/spotify.go
Normal file
87
tools/auth/spotify.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/spotify"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameSpotify] = wrapFactory(NewSpotifyProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Spotify)(nil)
|
||||
|
||||
// NameSpotify is the unique name of the Spotify provider.
|
||||
const NameSpotify string = "spotify"
|
||||
|
||||
// Spotify allows authentication via Spotify OAuth2.
|
||||
type Spotify struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewSpotifyProvider creates a new Spotify provider instance with some defaults.
|
||||
func NewSpotifyProvider() *Spotify {
|
||||
return &Spotify{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Spotify",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
"user-read-private",
|
||||
// currently Spotify doesn't return information whether the email is verified or not
|
||||
// "user-read-email",
|
||||
},
|
||||
authURL: spotify.Endpoint.AuthURL,
|
||||
tokenURL: spotify.Endpoint.TokenURL,
|
||||
userInfoURL: "https://api.spotify.com/v1/me",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Spotify's user api.
|
||||
//
|
||||
// API reference: https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile
|
||||
func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"display_name"`
|
||||
Images []struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"images"`
|
||||
// don't map the email because per the official docs
|
||||
// the email field is "unverified" and there is no proof
|
||||
// that it actually belongs to the user
|
||||
// Email string `json:"email"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if len(extracted.Images) > 0 {
|
||||
user.AvatarURL = extracted.Images[0].URL
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
85
tools/auth/strava.go
Normal file
85
tools/auth/strava.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameStrava] = wrapFactory(NewStravaProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Strava)(nil)
|
||||
|
||||
// NameStrava is the unique name of the Strava provider.
|
||||
const NameStrava string = "strava"
|
||||
|
||||
// Strava allows authentication via Strava OAuth2.
|
||||
type Strava struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewStravaProvider creates new Strava provider instance with some defaults.
|
||||
func NewStravaProvider() *Strava {
|
||||
return &Strava{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Strava",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
"profile:read_all",
|
||||
},
|
||||
authURL: "https://www.strava.com/oauth/authorize",
|
||||
tokenURL: "https://www.strava.com/api/v3/oauth/token",
|
||||
userInfoURL: "https://www.strava.com/api/v3/athlete",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Strava's user api.
|
||||
//
|
||||
// API reference: https://developers.strava.com/docs/authentication/
|
||||
func (p *Strava) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id int64 `json:"id"`
|
||||
FirstName string `json:"firstname"`
|
||||
LastName string `json:"lastname"`
|
||||
Username string `json:"username"`
|
||||
ProfileImageURL string `json:"profile"`
|
||||
|
||||
// At the time of writing, Strava OAuth2 doesn't support returning the user email address
|
||||
// Email string `json:"email"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Name: extracted.FirstName + " " + extracted.LastName,
|
||||
Username: extracted.Username,
|
||||
AvatarURL: extracted.ProfileImageURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if extracted.Id != 0 {
|
||||
user.Id = strconv.FormatInt(extracted.Id, 10)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
102
tools/auth/trakt.go
Normal file
102
tools/auth/trakt.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameTrakt] = wrapFactory(NewTraktProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Trakt)(nil)
|
||||
|
||||
// NameTrakt is the unique name of the Trakt provider.
|
||||
const NameTrakt string = "trakt"
|
||||
|
||||
// Trakt allows authentication via Trakt OAuth2.
|
||||
type Trakt struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewTraktProvider creates new Trakt provider instance with some defaults.
|
||||
func NewTraktProvider() *Trakt {
|
||||
return &Trakt{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Trakt",
|
||||
pkce: true,
|
||||
authURL: "https://trakt.tv/oauth/authorize",
|
||||
tokenURL: "https://api.trakt.tv/oauth/token",
|
||||
userInfoURL: "https://api.trakt.tv/users/settings",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on Trakt's user settings API.
|
||||
// API reference: https://trakt.docs.apiary.io/#reference/users/settings/retrieve-settings
|
||||
func (p *Trakt) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
User struct {
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Ids struct {
|
||||
Slug string `json:"slug"`
|
||||
UUID string `json:"uuid"`
|
||||
} `json:"ids"`
|
||||
Images struct {
|
||||
Avatar struct {
|
||||
Full string `json:"full"`
|
||||
} `json:"avatar"`
|
||||
} `json:"images"`
|
||||
} `json:"user"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.User.Ids.UUID,
|
||||
Username: extracted.User.Username,
|
||||
Name: extracted.User.Name,
|
||||
AvatarURL: extracted.User.Images.Avatar.Full,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
|
||||
//
|
||||
// This differ from BaseProvider because Trakt requires a number of
|
||||
// mandatory headers for all requests
|
||||
// (https://trakt.docs.apiary.io/#introduction/required-headers).
|
||||
func (p *Trakt) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(p.ctx, "GET", p.userInfoURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-type", "application/json")
|
||||
req.Header.Set("trakt-api-key", p.clientId)
|
||||
req.Header.Set("trakt-api-version", "2")
|
||||
|
||||
return p.sendRawUserInfoRequest(req, token)
|
||||
}
|
100
tools/auth/twitch.go
Normal file
100
tools/auth/twitch.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/twitch"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameTwitch] = wrapFactory(NewTwitchProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Twitch)(nil)
|
||||
|
||||
// NameTwitch is the unique name of the Twitch provider.
|
||||
const NameTwitch string = "twitch"
|
||||
|
||||
// Twitch allows authentication via Twitch OAuth2.
|
||||
type Twitch struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewTwitchProvider creates new Twitch provider instance with some defaults.
|
||||
func NewTwitchProvider() *Twitch {
|
||||
return &Twitch{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Twitch",
|
||||
pkce: true,
|
||||
scopes: []string{"user:read:email"},
|
||||
authURL: twitch.Endpoint.AuthURL,
|
||||
tokenURL: twitch.Endpoint.TokenURL,
|
||||
userInfoURL: "https://api.twitch.tv/helix/users",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the Twitch's user api.
|
||||
//
|
||||
// API reference: https://dev.twitch.tv/docs/api/reference#get-users
|
||||
func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Data []struct {
|
||||
Id string `json:"id"`
|
||||
Login string `json:"login"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
} `json:"data"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(extracted.Data) == 0 {
|
||||
return nil, errors.New("failed to fetch AuthUser data")
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data[0].Id,
|
||||
Name: extracted.Data[0].DisplayName,
|
||||
Username: extracted.Data[0].Login,
|
||||
Email: extracted.Data[0].Email,
|
||||
AvatarURL: extracted.Data[0].ProfileImageURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// FetchRawUserInfo implements Provider.FetchRawUserInfo interface method.
|
||||
//
|
||||
// This differ from BaseProvider because Twitch requires the Client-Id header.
|
||||
func (p *Twitch) FetchRawUserInfo(token *oauth2.Token) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(p.ctx, "GET", p.userInfoURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Client-Id", p.clientId)
|
||||
|
||||
return p.sendRawUserInfoRequest(req, token)
|
||||
}
|
87
tools/auth/twitter.go
Normal file
87
tools/auth/twitter.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameTwitter] = wrapFactory(NewTwitterProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Twitter)(nil)
|
||||
|
||||
// NameTwitter is the unique name of the Twitter provider.
|
||||
const NameTwitter string = "twitter"
|
||||
|
||||
// Twitter allows authentication via Twitter OAuth2.
|
||||
type Twitter struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewTwitterProvider creates new Twitter provider instance with some defaults.
|
||||
func NewTwitterProvider() *Twitter {
|
||||
return &Twitter{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Twitter",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
"users.read",
|
||||
|
||||
// we don't actually use this scope, but for some reason it is required by the `/2/users/me` endpoint
|
||||
// (see https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me)
|
||||
"tweet.read",
|
||||
},
|
||||
authURL: "https://twitter.com/i/oauth2/authorize",
|
||||
tokenURL: "https://api.twitter.com/2/oauth2/token",
|
||||
userInfoURL: "https://api.twitter.com/2/users/me?user.fields=id,name,username,profile_image_url",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Twitter's user api.
|
||||
//
|
||||
// API reference: https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me
|
||||
func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Data struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
ProfileImageURL string `json:"profile_image_url"`
|
||||
|
||||
// NB! At the time of writing, Twitter OAuth2 doesn't support returning the user email address
|
||||
// (see https://twittercommunity.com/t/which-api-to-get-user-after-oauth2-authorization/162417/33)
|
||||
// Email string `json:"email"`
|
||||
} `json:"data"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data.Id,
|
||||
Name: extracted.Data.Name,
|
||||
Username: extracted.Data.Username,
|
||||
AvatarURL: extracted.Data.ProfileImageURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
94
tools/auth/vk.go
Normal file
94
tools/auth/vk.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/vk"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameVK] = wrapFactory(NewVKProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*VK)(nil)
|
||||
|
||||
// NameVK is the unique name of the VK provider.
|
||||
const NameVK string = "vk"
|
||||
|
||||
// VK allows authentication via VK OAuth2.
|
||||
type VK struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewVKProvider creates new VK provider instance with some defaults.
|
||||
//
|
||||
// Docs: https://dev.vk.com/api/oauth-parameters
|
||||
func NewVKProvider() *VK {
|
||||
return &VK{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "ВКонтакте",
|
||||
pkce: false, // VK currently doesn't support PKCE and throws an error if PKCE params are send
|
||||
scopes: []string{"email"},
|
||||
authURL: vk.Endpoint.AuthURL,
|
||||
tokenURL: vk.Endpoint.TokenURL,
|
||||
userInfoURL: "https://api.vk.com/method/users.get?fields=photo_max,screen_name&v=5.131",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on VK's user api.
|
||||
//
|
||||
// API reference: https://dev.vk.com/method/users.get
|
||||
func (p *VK) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Response []struct {
|
||||
Id int64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Username string `json:"screen_name"`
|
||||
AvatarURL string `json:"photo_max"`
|
||||
} `json:"response"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(extracted.Response) == 0 {
|
||||
return nil, errors.New("missing response entry")
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.FormatInt(extracted.Response[0].Id, 10),
|
||||
Name: strings.TrimSpace(extracted.Response[0].FirstName + " " + extracted.Response[0].LastName),
|
||||
Username: extracted.Response[0].Username,
|
||||
AvatarURL: extracted.Response[0].AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if email := token.Extra("email"); email != nil {
|
||||
user.Email = fmt.Sprint(email)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
90
tools/auth/wakatime.go
Normal file
90
tools/auth/wakatime.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameWakatime] = wrapFactory(NewWakatimeProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Wakatime)(nil)
|
||||
|
||||
// NameWakatime is the unique name of the Wakatime provider.
|
||||
const NameWakatime = "wakatime"
|
||||
|
||||
// Wakatime is an auth provider for Wakatime.
|
||||
type Wakatime struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewWakatimeProvider creates a new Wakatime provider instance with some defaults.
|
||||
func NewWakatimeProvider() *Wakatime {
|
||||
return &Wakatime{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "WakaTime",
|
||||
pkce: true,
|
||||
scopes: []string{"email"},
|
||||
authURL: "https://wakatime.com/oauth/authorize",
|
||||
tokenURL: "https://wakatime.com/oauth/token",
|
||||
userInfoURL: "https://wakatime.com/api/v1/users/current",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on the Wakatime's user API.
|
||||
//
|
||||
// API reference: https://wakatime.com/developers#users
|
||||
func (p *Wakatime) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Data struct {
|
||||
Id string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Photo string `json:"photo"`
|
||||
IsPhotoPublic bool `json:"photo_public"`
|
||||
IsEmailConfirmed bool `json:"is_email_confirmed"`
|
||||
} `json:"data"`
|
||||
}{}
|
||||
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Data.Id,
|
||||
Name: extracted.Data.DisplayName,
|
||||
Username: extracted.Data.Username,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
// note: we don't check for is_email_public field because PocketBase
|
||||
// has its own emailVisibility flag which is false by default
|
||||
if extracted.Data.IsEmailConfirmed {
|
||||
user.Email = extracted.Data.Email
|
||||
}
|
||||
|
||||
if extracted.Data.IsPhotoPublic {
|
||||
user.AvatarURL = extracted.Data.Photo
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
84
tools/auth/yandex.go
Normal file
84
tools/auth/yandex.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/yandex"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Providers[NameYandex] = wrapFactory(NewYandexProvider)
|
||||
}
|
||||
|
||||
var _ Provider = (*Yandex)(nil)
|
||||
|
||||
// NameYandex is the unique name of the Yandex provider.
|
||||
const NameYandex string = "yandex"
|
||||
|
||||
// Yandex allows authentication via Yandex OAuth2.
|
||||
type Yandex struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
// NewYandexProvider creates new Yandex provider instance with some defaults.
|
||||
//
|
||||
// Docs: https://yandex.ru/dev/id/doc/en/
|
||||
func NewYandexProvider() *Yandex {
|
||||
return &Yandex{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
displayName: "Yandex",
|
||||
pkce: true,
|
||||
scopes: []string{"login:email", "login:avatar", "login:info"},
|
||||
authURL: yandex.Endpoint.AuthURL,
|
||||
tokenURL: yandex.Endpoint.TokenURL,
|
||||
userInfoURL: "https://login.yandex.ru/info",
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on Yandex's user api.
|
||||
//
|
||||
// API reference: https://yandex.ru/dev/id/doc/en/user-information#response-format
|
||||
func (p *Yandex) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rawUser := map[string]any{}
|
||||
if err := json.Unmarshal(data, &rawUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"real_name"`
|
||||
Username string `json:"login"`
|
||||
Email string `json:"default_email"`
|
||||
IsAvatarEmpty bool `json:"is_avatar_empty"`
|
||||
AvatarId string `json:"default_avatar_id"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: extracted.Id,
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
if !extracted.IsAvatarEmpty {
|
||||
user.AvatarURL = "https://avatars.yandex.net/get-yapic/" + extracted.AvatarId + "/islands-200"
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue