[#33] added Twitter OAuth2 provider

This commit is contained in:
Gani Georgiev 2022-09-01 16:46:06 +03:00
parent f56c52a1f7
commit 07ac5bf6a2
6 changed files with 80 additions and 2 deletions

View File

@ -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.
@ -111,6 +112,10 @@ func NewSettings() *Settings {
Enabled: false,
AllowRegistrations: true,
},
TwitterAuth: AuthProviderConfig{
Enabled: false,
AllowRegistrations: true,
},
}
}
@ -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),
)
}
@ -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
@ -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,
}
}

View File

@ -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()
@ -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)
@ -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 {
@ -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":"support@example.com","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":"support@example.com","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)
@ -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()
@ -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)

Binary file not shown.

View File

@ -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)
}

60
tools/auth/twitter.go Normal file
View File

@ -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
}

View File

@ -37,6 +37,8 @@
confirmClose = true;
activeTab = user.isNew || user.email ? accountTab : providersTab;
return panel?.show();
}