initial public commit

This commit is contained in:
Gani Georgiev
2022-07-07 00:19:05 +03:00
commit 3d07f0211d
484 changed files with 92412 additions and 0 deletions
+96
View File
@@ -0,0 +1,96 @@
package auth
import (
"errors"
"net/http"
"golang.org/x/oauth2"
)
// AuthUser defines a standardized oauth2 user data structure.
type AuthUser struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
AvatarUrl string `json:"avatarUrl"`
}
// Provider defines a common interface for an OAuth2 client.
type Provider interface {
// 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)
// ClientId 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)
// UserApiUrl returns the provider's user info api url.
UserApiUrl() string
// SetUserApiUrl sets the provider's UserApiUrl.
SetUserApiUrl(url string)
// 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)
// FetchRawUserData requests and marshalizes into `result` the
// the OAuth user api response.
FetchRawUserData(token *oauth2.Token, result any) error
// FetchAuthUser is similar to FetchRawUserData, but normalizes and
// marshalizes the user api response into a standardized AuthUser struct.
FetchAuthUser(token *oauth2.Token) (user *AuthUser, err error)
}
// NewProviderByName returns a new preconfigured provider instance by its name identifier.
func NewProviderByName(name string) (Provider, error) {
switch name {
case NameGoogle:
return NewGoogleProvider(), nil
case NameFacebook:
return NewFacebookProvider(), nil
case NameGithub:
return NewGithubProvider(), nil
case NameGitlab:
return NewGitlabProvider(), nil
default:
return nil, errors.New("Missing provider " + name)
}
}
+57
View File
@@ -0,0 +1,57 @@
package auth_test
import (
"testing"
"github.com/pocketbase/pocketbase/tools/auth"
)
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")
}
}
+138
View File
@@ -0,0 +1,138 @@
package auth
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"golang.org/x/oauth2"
)
// baseProvider defines common fields and methods used by OAuth2 client providers.
type baseProvider struct {
scopes []string
clientId string
clientSecret string
redirectUrl string
authUrl string
tokenUrl string
userApiUrl string
}
// Scopes implements Provider.Scopes interface.
func (p *baseProvider) Scopes() []string {
return p.scopes
}
// SetScopes implements Provider.SetScopes interface.
func (p *baseProvider) SetScopes(scopes []string) {
p.scopes = scopes
}
// ClientId implements Provider.ClientId interface.
func (p *baseProvider) ClientId() string {
return p.clientId
}
// SetClientId implements Provider.SetClientId interface.
func (p *baseProvider) SetClientId(clientId string) {
p.clientId = clientId
}
// ClientSecret implements Provider.ClientSecret interface.
func (p *baseProvider) ClientSecret() string {
return p.clientSecret
}
// SetClientSecret implements Provider.SetClientSecret interface.
func (p *baseProvider) SetClientSecret(secret string) {
p.clientSecret = secret
}
// RedirectUrl implements Provider.RedirectUrl interface.
func (p *baseProvider) RedirectUrl() string {
return p.redirectUrl
}
// SetRedirectUrl implements Provider.SetRedirectUrl interface.
func (p *baseProvider) SetRedirectUrl(url string) {
p.redirectUrl = url
}
// AuthUrl implements Provider.AuthUrl interface.
func (p *baseProvider) AuthUrl() string {
return p.authUrl
}
// SetAuthUrl implements Provider.SetAuthUrl interface.
func (p *baseProvider) SetAuthUrl(url string) {
p.authUrl = url
}
// TokenUrl implements Provider.TokenUrl interface.
func (p *baseProvider) TokenUrl() string {
return p.tokenUrl
}
// SetTokenUrl implements Provider.SetTokenUrl interface.
func (p *baseProvider) SetTokenUrl(url string) {
p.tokenUrl = url
}
// UserApiUrl implements Provider.UserApiUrl interface.
func (p *baseProvider) UserApiUrl() string {
return p.userApiUrl
}
// SetUserApiUrl implements Provider.SetUserApiUrl interface.
func (p *baseProvider) SetUserApiUrl(url string) {
p.userApiUrl = url
}
// BuildAuthUrl implements Provider.BuildAuthUrl interface.
func (p *baseProvider) BuildAuthUrl(state string, opts ...oauth2.AuthCodeOption) string {
return p.oauth2Config().AuthCodeURL(state, opts...)
}
// FetchToken implements Provider.FetchToken interface.
func (p *baseProvider) FetchToken(code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return p.oauth2Config().Exchange(context.Background(), code, opts...)
}
// Client implements Provider.Client interface.
func (p *baseProvider) Client(token *oauth2.Token) *http.Client {
return p.oauth2Config().Client(context.Background(), token)
}
// FetchRawUserData implements Provider.FetchRawUserData interface.
func (p *baseProvider) FetchRawUserData(token *oauth2.Token, result any) error {
client := p.Client(token)
response, err := client.Get(p.userApiUrl)
if err != nil {
return err
}
defer response.Body.Close()
content, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
return json.Unmarshal(content, &result)
}
// 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,
},
}
}
+183
View File
@@ -0,0 +1,183 @@
package auth
import (
"testing"
"golang.org/x/oauth2"
)
func TestScopes(t *testing.T) {
b := baseProvider{}
before := b.Scopes()
if len(before) != 0 {
t.Errorf("Expected 0 scopes, got %v", before)
}
b.SetScopes([]string{"test1", "test2"})
after := b.Scopes()
if len(after) != 2 {
t.Errorf("Expected 2 scopes, got %v", after)
}
}
func TestClientId(t *testing.T) {
b := baseProvider{}
before := b.ClientId()
if before != "" {
t.Errorf("Expected clientId to be empty, got %v", before)
}
b.SetClientId("test")
after := b.ClientId()
if after != "test" {
t.Errorf("Expected clientId to be 'test', got %v", after)
}
}
func TestClientSecret(t *testing.T) {
b := baseProvider{}
before := b.ClientSecret()
if before != "" {
t.Errorf("Expected clientSecret to be empty, got %v", before)
}
b.SetClientSecret("test")
after := b.ClientSecret()
if after != "test" {
t.Errorf("Expected clientSecret to be 'test', got %v", after)
}
}
func TestRedirectUrl(t *testing.T) {
b := baseProvider{}
before := b.RedirectUrl()
if before != "" {
t.Errorf("Expected RedirectUrl to be empty, got %v", before)
}
b.SetRedirectUrl("test")
after := b.RedirectUrl()
if after != "test" {
t.Errorf("Expected RedirectUrl to be 'test', got %v", after)
}
}
func TestAuthUrl(t *testing.T) {
b := baseProvider{}
before := b.AuthUrl()
if before != "" {
t.Errorf("Expected authUrl to be empty, got %v", before)
}
b.SetAuthUrl("test")
after := b.AuthUrl()
if after != "test" {
t.Errorf("Expected authUrl to be 'test', got %v", after)
}
}
func TestTokenUrl(t *testing.T) {
b := baseProvider{}
before := b.TokenUrl()
if before != "" {
t.Errorf("Expected tokenUrl to be empty, got %v", before)
}
b.SetTokenUrl("test")
after := b.TokenUrl()
if after != "test" {
t.Errorf("Expected tokenUrl to be 'test', got %v", after)
}
}
func TestUserApiUrl(t *testing.T) {
b := baseProvider{}
before := b.UserApiUrl()
if before != "" {
t.Errorf("Expected userApiUrl to be empty, got %v", before)
}
b.SetUserApiUrl("test")
after := b.UserApiUrl()
if after != "test" {
t.Errorf("Expected userApiUrl to be 'test', got %v", after)
}
}
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)
}
}
+51
View File
@@ -0,0 +1,51 @@
package auth
import (
"golang.org/x/oauth2"
)
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{
scopes: []string{"email"},
authUrl: "https://www.facebook.com/dialog/oauth",
tokenUrl: "https://graph.facebook.com/oauth/access_token",
userApiUrl: "https://graph.facebook.com/me?fields=name,email,picture.type(large)",
}}
}
// FetchAuthUser returns an AuthUser instance based on the Facebook's user api.
func (p *Facebook) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://developers.facebook.com/docs/graph-api/reference/user/
rawData := struct {
Id string
Name string
Email string
Picture struct {
Data struct{ Url string }
}
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.Id,
Name: rawData.Name,
Email: rawData.Email,
AvatarUrl: rawData.Picture.Data.Url,
}
return user, nil
}
+87
View File
@@ -0,0 +1,87 @@
package auth
import (
"encoding/json"
"io/ioutil"
"strconv"
"golang.org/x/oauth2"
)
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{
scopes: []string{"user"},
authUrl: "https://github.com/login/oauth/authorize",
tokenUrl: "https://github.com/login/oauth/access_token",
userApiUrl: "https://api.github.com/user",
}}
}
// FetchAuthUser returns an AuthUser instance based the Github's user api.
func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
rawData := struct {
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 {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Name: rawData.Name,
Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl,
}
// in case user set "Keep my email address private",
// email should be retrieved via extra API request
if user.Email == "" {
client := p.Client(token)
response, err := client.Get(p.userApiUrl + "/emails")
if err != nil {
return user, err
}
defer response.Body.Close()
content, err := ioutil.ReadAll(response.Body)
if err != nil {
return user, err
}
emails := []struct {
Email string
Verified bool
Primary bool
}{}
if err := json.Unmarshal(content, &emails); err != nil {
return user, err
}
// extract the verified primary email
for _, email := range emails {
if email.Verified && email.Primary {
user.Email = email.Email
break
}
}
}
return user, nil
}
+51
View File
@@ -0,0 +1,51 @@
package auth
import (
"strconv"
"golang.org/x/oauth2"
)
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{
scopes: []string{"read_user"},
authUrl: "https://gitlab.com/oauth/authorize",
tokenUrl: "https://gitlab.com/oauth/token",
userApiUrl: "https://gitlab.com/api/v4/user",
}}
}
// FetchAuthUser returns an AuthUser instance based the Gitlab's user api.
func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://docs.gitlab.com/ee/api/users.html#for-admin
rawData := struct {
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 {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Name: rawData.Name,
Email: rawData.Email,
AvatarUrl: rawData.AvatarUrl,
}
return user, nil
}
+52
View File
@@ -0,0 +1,52 @@
package auth
import (
"golang.org/x/oauth2"
)
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{
scopes: []string{
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
},
authUrl: "https://accounts.google.com/o/oauth2/auth",
tokenUrl: "https://accounts.google.com/o/oauth2/token",
userApiUrl: "https://www.googleapis.com/oauth2/v1/userinfo",
}}
}
// FetchAuthUser returns an AuthUser instance based the Google's user api.
func (p *Google) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
// https://cloud.google.com/identity-platform/docs/reference/rest/v1/UserInfo
rawData := struct {
LocalId string `json:"localId"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
PhotoUrl string `json:"photoUrl"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
return nil, err
}
user := &AuthUser{
Id: rawData.LocalId,
Name: rawData.DisplayName,
Email: rawData.Email,
AvatarUrl: rawData.PhotoUrl,
}
return user, nil
}