Skip to content

Commit

Permalink
[pocketbase#33] added Twitter OAuth2 provider
Browse files Browse the repository at this point in the history
  • Loading branch information
ganigeorgiev committed Sep 1, 2022
1 parent f56c52a commit 07ac5bf
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 2 deletions.
8 changes: 8 additions & 0 deletions core/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Settings struct {
GithubAuth AuthProviderConfig `form:"githubAuth" json:"githubAuth"`
GitlabAuth AuthProviderConfig `form:"gitlabAuth" json:"gitlabAuth"`
DiscordAuth AuthProviderConfig `form:"discordAuth" json:"discordAuth"`
TwitterAuth AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"`
}

// NewSettings creates and returns a new default Settings instance.
Expand Down Expand Up @@ -111,6 +112,10 @@ func NewSettings() *Settings {
Enabled: false,
AllowRegistrations: true,
},
TwitterAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
}
}

Expand All @@ -136,6 +141,7 @@ func (s *Settings) Validate() error {
validation.Field(&s.GithubAuth),
validation.Field(&s.GitlabAuth),
validation.Field(&s.DiscordAuth),
validation.Field(&s.TwitterAuth),
)
}

Expand Down Expand Up @@ -185,6 +191,7 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.GithubAuth.ClientSecret,
&clone.GitlabAuth.ClientSecret,
&clone.DiscordAuth.ClientSecret,
&clone.TwitterAuth.ClientSecret,
}

// mask all sensitive fields
Expand All @@ -209,6 +216,7 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
auth.NameGithub: s.GithubAuth,
auth.NameGitlab: s.GitlabAuth,
auth.NameDiscord: s.DiscordAuth,
auth.NameTwitter: s.TwitterAuth,
}
}

Expand Down
10 changes: 8 additions & 2 deletions core/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func TestSettingsValidate(t *testing.T) {
s.GitlabAuth.ClientId = ""
s.DiscordAuth.Enabled = true
s.DiscordAuth.ClientId = ""
s.TwitterAuth.Enabled = true
s.TwitterAuth.ClientId = ""

// check if Validate() is triggering the members validate methods.
err := s.Validate()
Expand Down Expand Up @@ -103,6 +105,8 @@ func TestSettingsMerge(t *testing.T) {
s2.GitlabAuth.ClientId = "gitlab_test"
s2.DiscordAuth.Enabled = true
s2.DiscordAuth.ClientId = "discord_test"
s2.TwitterAuth.Enabled = true
s2.TwitterAuth.ClientId = "twitter_test"

if err := s1.Merge(s2); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -169,6 +173,7 @@ func TestSettingsRedactClone(t *testing.T) {
s1.GithubAuth.ClientSecret = "test123"
s1.GitlabAuth.ClientSecret = "test123"
s1.DiscordAuth.ClientSecret = "test123"
s1.TwitterAuth.ClientSecret = "test123"

s2, err := s1.RedactClone()
if err != nil {
Expand All @@ -180,7 +185,7 @@ func TestSettingsRedactClone(t *testing.T) {
t.Fatal(err)
}

expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"[email protected]","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/users/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/users/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/users/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":7},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"userAuthToken":{"secret":"******","duration":1209600},"userPasswordResetToken":{"secret":"******","duration":1800},"userEmailChangeToken":{"secret":"******","duration":1800},"userVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":true,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":8},"googleAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"facebookAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"githubAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"discordAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"}}`
expected := `{"meta":{"appName":"test123","appUrl":"http://localhost:8090","hideControls":false,"senderName":"Support","senderAddress":"[email protected]","verificationTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eThank you for joining us at {APP_NAME}.\u003c/p\u003e\n\u003cp\u003eClick on the button below to verify your email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eVerify\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Verify your {APP_NAME} email","actionUrl":"{APP_URL}/_/#/users/confirm-verification/{TOKEN}"},"resetPasswordTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to reset your password.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eReset password\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to reset your password, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Reset your {APP_NAME} password","actionUrl":"{APP_URL}/_/#/users/confirm-password-reset/{TOKEN}"},"confirmEmailChangeTemplate":{"body":"\u003cp\u003eHello,\u003c/p\u003e\n\u003cp\u003eClick on the button below to confirm your new email address.\u003c/p\u003e\n\u003cp\u003e\n \u003ca class=\"btn\" href=\"{ACTION_URL}\" target=\"_blank\" rel=\"noopener\"\u003eConfirm new email\u003c/a\u003e\n\u003c/p\u003e\n\u003cp\u003e\u003ci\u003eIf you didn't ask to change your email address, you can ignore this email.\u003c/i\u003e\u003c/p\u003e\n\u003cp\u003e\n Thanks,\u003cbr/\u003e\n {APP_NAME} team\n\u003c/p\u003e","subject":"Confirm your {APP_NAME} new email address","actionUrl":"{APP_URL}/_/#/users/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":7},"smtp":{"enabled":false,"host":"smtp.example.com","port":587,"username":"","password":"******","tls":true},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","secret":"******","forcePathStyle":false},"adminAuthToken":{"secret":"******","duration":1209600},"adminPasswordResetToken":{"secret":"******","duration":1800},"userAuthToken":{"secret":"******","duration":1209600},"userPasswordResetToken":{"secret":"******","duration":1800},"userEmailChangeToken":{"secret":"******","duration":1800},"userVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":true,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":8},"googleAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"facebookAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"githubAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"discordAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"},"twitterAuth":{"enabled":false,"allowRegistrations":true,"clientSecret":"******"}}`

if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected %v, got \n%v", expected, encodedStr)
Expand All @@ -196,6 +201,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.GitlabAuth.ClientId = "gitlab_test"
s.GitlabAuth.Enabled = true
s.DiscordAuth.ClientId = "discord_test"
s.TwitterAuth.ClientId = "twitter_test"

result := s.NamedAuthProviderConfigs()

Expand All @@ -204,7 +210,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
t.Fatal(err)
}

expected := `{"discord":{"enabled":false,"allowRegistrations":true,"clientId":"discord_test"},"facebook":{"enabled":false,"allowRegistrations":true,"clientId":"facebook_test"},"github":{"enabled":false,"allowRegistrations":true,"clientId":"github_test"},"gitlab":{"enabled":true,"allowRegistrations":true,"clientId":"gitlab_test"},"google":{"enabled":false,"allowRegistrations":true,"clientId":"google_test"}}`
expected := `{"discord":{"enabled":false,"allowRegistrations":true,"clientId":"discord_test"},"facebook":{"enabled":false,"allowRegistrations":true,"clientId":"facebook_test"},"github":{"enabled":false,"allowRegistrations":true,"clientId":"github_test"},"gitlab":{"enabled":true,"allowRegistrations":true,"clientId":"gitlab_test"},"google":{"enabled":false,"allowRegistrations":true,"clientId":"google_test"},"twitter":{"enabled":false,"allowRegistrations":true,"clientId":"twitter_test"}}`

if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected the same serialization, got %v", encodedStr)
Expand Down
Binary file modified tests/data/data.db
Binary file not shown.
2 changes: 2 additions & 0 deletions tools/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func NewProviderByName(name string) (Provider, error) {
return NewGitlabProvider(), nil
case NameDiscord:
return NewDiscordProvider(), nil
case NameTwitter:
return NewTwitterProvider(), nil
default:
return nil, errors.New("Missing provider " + name)
}
Expand Down
60 changes: 60 additions & 0 deletions tools/auth/twitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package auth

import (
"golang.org/x/oauth2"
)

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{
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",
userApiUrl: "https://api.twitter.com/2/users/me?user.fields=id,name,profile_image_url",
}}
}

// FetchAuthUser returns an AuthUser instance based on the Twitter's user api.
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 struct {
Id string `json:"id"`
Name string `json:"name"`
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 := p.FetchRawUserData(token, &rawData); err != nil {
return nil, err
}

user := &AuthUser{
Id: rawData.Data.Id,
Name: rawData.Data.Name,
Email: rawData.Data.Email,
AvatarUrl: rawData.Data.ProfileImageUrl,
}

return user, nil
}
2 changes: 2 additions & 0 deletions ui/src/components/users/UserUpsertPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
confirmClose = true;
activeTab = user.isNew || user.email ? accountTab : providersTab;
return panel?.show();
}
Expand Down

0 comments on commit 07ac5bf

Please sign in to comment.