1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-28 10:03:42 +02:00

[#654] updated OAuth2 providers to return the access token and raw user data

This commit is contained in:
Gani Georgiev 2022-11-30 15:16:09 +02:00
parent 9ba710cdc5
commit 799e1d96f8
12 changed files with 250 additions and 115 deletions

View File

@ -9,11 +9,13 @@ import (
// AuthUser defines a standardized oauth2 user data structure.
type AuthUser struct {
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
Id string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
RawUser map[string]any `json:"rawUser"`
AccessToken string `json:"accessToken"`
}
// Provider defines a common interface for an OAuth2 client.
@ -73,7 +75,7 @@ type Provider interface {
// FetchRawUserData requests and marshalizes into `result` the
// the OAuth user api response.
FetchRawUserData(token *oauth2.Token, result any) error
FetchRawUserData(token *oauth2.Token) ([]byte, error)
// FetchAuthUser is similar to FetchRawUserData, but normalizes and
// marshalizes the user api response into a standardized AuthUser struct.

View File

@ -2,7 +2,6 @@ package auth
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@ -107,42 +106,41 @@ func (p *baseProvider) Client(token *oauth2.Token) *http.Client {
}
// FetchRawUserData implements Provider.FetchRawUserData interface.
func (p *baseProvider) FetchRawUserData(token *oauth2.Token, result any) error {
func (p *baseProvider) FetchRawUserData(token *oauth2.Token) ([]byte, error) {
req, err := http.NewRequest("GET", p.userApiUrl, nil)
if err != nil {
return err
return nil, err
}
return p.sendRawUserDataRequest(req, token, result)
return p.sendRawUserDataRequest(req, token)
}
// sendRawUserDataRequest sends the specified request and
// unmarshal the response body into result.
func (p *baseProvider) sendRawUserDataRequest(req *http.Request, token *oauth2.Token, result any) error {
// sendRawUserDataRequest sends the specified user data request and return its raw response body.
func (p *baseProvider) sendRawUserDataRequest(req *http.Request, token *oauth2.Token) ([]byte, error) {
client := p.Client(token)
response, err := client.Do(req)
if err != nil {
return err
return nil, err
}
defer response.Body.Close()
content, err := ioutil.ReadAll(response.Body)
result, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
return nil, err
}
// http.Client.Get doesn't treat non 2xx responses as error
if response.StatusCode >= 400 {
return fmt.Errorf(
return nil, fmt.Errorf(
"Failed to fetch OAuth2 user profile via %s (%d):\n%s",
p.userApiUrl,
response.StatusCode,
string(content),
string(result),
)
}
return json.Unmarshal(content, &result)
return result, nil
}
// oauth2Config constructs a oauth2.Config instance based on the provider settings.

View File

@ -1,6 +1,7 @@
package auth
import (
"encoding/json"
"fmt"
"golang.org/x/oauth2"
@ -29,33 +30,48 @@ func NewDiscordProvider() *Discord {
}
// 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) {
// https://discord.com/developers/docs/resources/user#user-object
rawData := struct {
data, err := p.FetchRawUserData(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"`
Email string `json:"email"`
Verified bool `json:"verified"`
Avatar string `json:"avatar"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
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", rawData.Id, rawData.Avatar)
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", rawData.Username, rawData.Discriminator)
username := fmt.Sprintf("%s#%s", extracted.Username, extracted.Discriminator)
user := &AuthUser{
Id: rawData.Id,
Name: username,
Username: rawData.Username,
Email: rawData.Email,
AvatarUrl: avatarUrl,
Id: extracted.Id,
Name: username,
Username: extracted.Username,
AvatarUrl: avatarUrl,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
if extracted.Verified {
user.Email = extracted.Email
}
return user, nil

View File

@ -1,6 +1,8 @@
package auth
import (
"encoding/json"
"golang.org/x/oauth2"
"golang.org/x/oauth2/facebook"
)
@ -26,9 +28,20 @@ func NewFacebookProvider() *Facebook {
}
// 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) {
// https://developers.facebook.com/docs/graph-api/reference/user/
rawData := struct {
data, err := p.FetchRawUserData(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
@ -36,16 +49,17 @@ func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
Data struct{ Url string }
}
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.Id,
Name: rawData.Name,
Email: rawData.Email,
AvatarUrl: rawData.Picture.Data.Url,
Id: extracted.Id,
Name: extracted.Name,
Email: extracted.Email,
AvatarUrl: extracted.Picture.Data.Url,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
return user, nil

View File

@ -30,26 +30,38 @@ func NewGithubProvider() *Github {
}
// 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) {
// https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
rawData := struct {
data, err := p.FetchRawUserData(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"`
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
AvatarUrl string `json:"avatar_url"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Name: rawData.Name,
Username: rawData.Login,
Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl,
Id: strconv.Itoa(extracted.Id),
Name: extracted.Name,
Username: extracted.Login,
Email: extracted.Email,
AvatarUrl: extracted.AvatarUrl,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
// in case user set "Keep my email address private",

View File

@ -1,6 +1,7 @@
package auth
import (
"encoding/json"
"strconv"
"golang.org/x/oauth2"
@ -27,26 +28,38 @@ func NewGitlabProvider() *Gitlab {
}
// 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) {
// https://docs.gitlab.com/ee/api/users.html#for-admin
rawData := struct {
data, err := p.FetchRawUserData(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 int `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Email string `json:"email"`
AvatarUrl string `json:"avatar_url"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Name: rawData.Name,
Username: rawData.Username,
Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl,
Id: strconv.Itoa(extracted.Id),
Name: extracted.Name,
Username: extracted.Username,
Email: extracted.Email,
AvatarUrl: extracted.AvatarUrl,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
return user, nil

View File

@ -1,6 +1,8 @@
package auth
import (
"encoding/json"
"golang.org/x/oauth2"
)
@ -29,22 +31,33 @@ func NewGoogleProvider() *Google {
// FetchAuthUser returns an AuthUser instance based the Google's user api.
func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
rawData := struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
}{}
data, err := p.FetchRawUserData(token)
if err != nil {
return nil, err
}
if err := p.FetchRawUserData(token, &rawData); err != nil {
rawUser := map[string]any{}
if err := json.Unmarshal(data, &rawUser); err != nil {
return nil, err
}
extracted := struct {
Id string
Name string
Email string
Picture string
}{}
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.Id,
Name: rawData.Name,
Email: rawData.Email,
AvatarUrl: rawData.Picture,
Id: extracted.Id,
Name: extracted.Name,
Email: extracted.Email,
AvatarUrl: extracted.Picture,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
return user, nil

View File

@ -1,6 +1,7 @@
package auth
import (
"encoding/json"
"strconv"
"golang.org/x/oauth2"
@ -28,9 +29,20 @@ func NewKakaoProvider() *Kakao {
}
// 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) {
// https://developers.kakao.com/docs/latest/en/kakaologin/rest-api#req-user-info-response
rawData := struct {
data, err := p.FetchRawUserData(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 int `json:"id"`
Profile struct {
Nickname string `json:"nickname"`
@ -42,18 +54,19 @@ func (p *Kakao) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
IsEmailValid bool `json:"is_email_valid"`
} `json:"kakao_account"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Username: rawData.Profile.Nickname,
AvatarUrl: rawData.Profile.ImageUrl,
Id: strconv.Itoa(extracted.Id),
Username: extracted.Profile.Nickname,
AvatarUrl: extracted.Profile.ImageUrl,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
if rawData.KakaoAccount.IsEmailValid && rawData.KakaoAccount.IsEmailVerified {
user.Email = rawData.KakaoAccount.Email
if extracted.KakaoAccount.IsEmailValid && extracted.KakaoAccount.IsEmailVerified {
user.Email = extracted.KakaoAccount.Email
}
return user, nil

View File

@ -1,6 +1,8 @@
package auth
import (
"encoding/json"
"golang.org/x/oauth2"
"golang.org/x/oauth2/microsoft"
)
@ -27,23 +29,35 @@ func NewMicrosoftProvider() *Microsoft {
}
// 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) {
// https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo
// explore graph: https://developer.microsoft.com/en-us/graph/graph-explorer
rawData := struct {
data, err := p.FetchRawUserData(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 := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.Id,
Name: rawData.Name,
Email: rawData.Email,
Id: extracted.Id,
Name: extracted.Name,
Email: extracted.Email,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
return user, nil

View File

@ -1,6 +1,8 @@
package auth
import (
"encoding/json"
"golang.org/x/oauth2"
"golang.org/x/oauth2/spotify"
)
@ -30,9 +32,20 @@ func NewSpotifyProvider() *Spotify {
}
// 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) {
// https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile
rawData := struct {
data, err := p.FetchRawUserData(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 {
@ -43,17 +56,18 @@ func (p *Spotify) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// that it actually belongs to the user
// Email string `json:"email"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.Id,
Name: rawData.Name,
Id: extracted.Id,
Name: extracted.Name,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
if len(rawData.Images) > 0 {
user.AvatarUrl = rawData.Images[0].Url
if len(extracted.Images) > 0 {
user.AvatarUrl = extracted.Images[0].Url
}
return user, nil

View File

@ -1,6 +1,7 @@
package auth
import (
"encoding/json"
"errors"
"net/http"
@ -29,9 +30,20 @@ func NewTwitchProvider() *Twitch {
}
// 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) {
// https://dev.twitch.tv/docs/api/reference#get-users
rawData := struct {
data, err := p.FetchRawUserData(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"`
@ -40,21 +52,22 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
ProfileImageUrl string `json:"profile_image_url"`
} `json:"data"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
if len(rawData.Data) == 0 {
if len(extracted.Data) == 0 {
return nil, errors.New("Failed to fetch AuthUser data")
}
user := &AuthUser{
Id: rawData.Data[0].Id,
Name: rawData.Data[0].DisplayName,
Username: rawData.Data[0].Login,
Email: rawData.Data[0].Email,
AvatarUrl: rawData.Data[0].ProfileImageUrl,
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,
}
return user, nil
@ -63,13 +76,13 @@ func (p *Twitch) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// FetchRawUserData implements Provider.FetchRawUserData interface.
//
// This differ from baseProvider because Twitch requires the `Client-Id` header.
func (p *Twitch) FetchRawUserData(token *oauth2.Token, result any) error {
func (p *Twitch) FetchRawUserData(token *oauth2.Token) ([]byte, error) {
req, err := http.NewRequest("GET", p.userApiUrl, nil)
if err != nil {
return err
return nil, err
}
req.Header.Set("Client-Id", p.clientId)
return p.sendRawUserDataRequest(req, token, result)
return p.sendRawUserDataRequest(req, token)
}

View File

@ -1,6 +1,8 @@
package auth
import (
"encoding/json"
"golang.org/x/oauth2"
)
@ -31,9 +33,20 @@ func NewTwitterProvider() *Twitter {
}
// 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) {
// https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me
rawData := struct {
data, err := p.FetchRawUserData(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"`
@ -42,20 +55,20 @@ func (p *Twitter) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// 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"`
// Email string `json:"email"`
} `json:"data"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
if err := json.Unmarshal(data, &extracted); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.Data.Id,
Name: rawData.Data.Name,
Username: rawData.Data.Username,
Email: rawData.Data.Email,
AvatarUrl: rawData.Data.ProfileImageUrl,
Id: extracted.Data.Id,
Name: extracted.Data.Name,
Username: extracted.Data.Username,
AvatarUrl: extracted.Data.ProfileImageUrl,
RawUser: rawUser,
AccessToken: token.AccessToken,
}
return user, nil