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

[#979] added Kakao OAuth2 provider

This commit is contained in:
Gani Georgiev 2022-11-13 13:05:06 +02:00
parent 521df149a2
commit 50fce1f3cf
7 changed files with 118 additions and 5 deletions

View File

@ -56,6 +56,7 @@ func TestSettingsList(t *testing.T) {
`"discordAuth":{`, `"discordAuth":{`,
`"microsoftAuth":{`, `"microsoftAuth":{`,
`"spotifyAuth":{`, `"spotifyAuth":{`,
`"kakaoAuth":{`,
`"secret":"******"`, `"secret":"******"`,
`"clientSecret":"******"`, `"clientSecret":"******"`,
}, },
@ -121,6 +122,7 @@ func TestSettingsSet(t *testing.T) {
`"discordAuth":{`, `"discordAuth":{`,
`"microsoftAuth":{`, `"microsoftAuth":{`,
`"spotifyAuth":{`, `"spotifyAuth":{`,
`"kakaoAuth":{`,
`"secret":"******"`, `"secret":"******"`,
`"clientSecret":"******"`, `"clientSecret":"******"`,
`"appName":"Acme"`, `"appName":"Acme"`,
@ -175,6 +177,7 @@ func TestSettingsSet(t *testing.T) {
`"discordAuth":{`, `"discordAuth":{`,
`"microsoftAuth":{`, `"microsoftAuth":{`,
`"spotifyAuth":{`, `"spotifyAuth":{`,
`"kakaoAuth":{`,
`"secret":"******"`, `"secret":"******"`,
`"clientSecret":"******"`, `"clientSecret":"******"`,
`"appName":"update_test"`, `"appName":"update_test"`,

View File

@ -41,6 +41,7 @@ type Settings struct {
TwitterAuth AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"` TwitterAuth AuthProviderConfig `form:"twitterAuth" json:"twitterAuth"`
MicrosoftAuth AuthProviderConfig `form:"microsoftAuth" json:"microsoftAuth"` MicrosoftAuth AuthProviderConfig `form:"microsoftAuth" json:"microsoftAuth"`
SpotifyAuth AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"` SpotifyAuth AuthProviderConfig `form:"spotifyAuth" json:"spotifyAuth"`
KakaoAuth AuthProviderConfig `form:"kakaoAuth" json:"kakaoAuth"`
} }
// NewSettings creates and returns a new default Settings instance. // NewSettings creates and returns a new default Settings instance.
@ -115,6 +116,9 @@ func NewSettings() *Settings {
SpotifyAuth: AuthProviderConfig{ SpotifyAuth: AuthProviderConfig{
Enabled: false, Enabled: false,
}, },
KakaoAuth: AuthProviderConfig{
Enabled: false,
},
} }
} }
@ -142,6 +146,7 @@ func (s *Settings) Validate() error {
validation.Field(&s.TwitterAuth), validation.Field(&s.TwitterAuth),
validation.Field(&s.MicrosoftAuth), validation.Field(&s.MicrosoftAuth),
validation.Field(&s.SpotifyAuth), validation.Field(&s.SpotifyAuth),
validation.Field(&s.KakaoAuth),
) )
} }
@ -194,6 +199,7 @@ func (s *Settings) RedactClone() (*Settings, error) {
&clone.TwitterAuth.ClientSecret, &clone.TwitterAuth.ClientSecret,
&clone.MicrosoftAuth.ClientSecret, &clone.MicrosoftAuth.ClientSecret,
&clone.SpotifyAuth.ClientSecret, &clone.SpotifyAuth.ClientSecret,
&clone.KakaoAuth.ClientSecret,
} }
// mask all sensitive fields // mask all sensitive fields
@ -221,6 +227,7 @@ func (s *Settings) NamedAuthProviderConfigs() map[string]AuthProviderConfig {
auth.NameTwitter: s.TwitterAuth, auth.NameTwitter: s.TwitterAuth,
auth.NameMicrosoft: s.MicrosoftAuth, auth.NameMicrosoft: s.MicrosoftAuth,
auth.NameSpotify: s.SpotifyAuth, auth.NameSpotify: s.SpotifyAuth,
auth.NameKakao: s.KakaoAuth,
} }
} }

View File

@ -43,6 +43,8 @@ func TestSettingsValidate(t *testing.T) {
s.MicrosoftAuth.ClientId = "" s.MicrosoftAuth.ClientId = ""
s.SpotifyAuth.Enabled = true s.SpotifyAuth.Enabled = true
s.SpotifyAuth.ClientId = "" s.SpotifyAuth.ClientId = ""
s.KakaoAuth.Enabled = true
s.KakaoAuth.ClientId = ""
// check if Validate() is triggering the members validate methods. // check if Validate() is triggering the members validate methods.
err := s.Validate() err := s.Validate()
@ -69,6 +71,7 @@ func TestSettingsValidate(t *testing.T) {
`"twitterAuth":{`, `"twitterAuth":{`,
`"microsoftAuth":{`, `"microsoftAuth":{`,
`"spotifyAuth":{`, `"spotifyAuth":{`,
`"kakaoAuth":{`,
} }
errBytes, _ := json.Marshal(err) errBytes, _ := json.Marshal(err)
@ -113,6 +116,8 @@ func TestSettingsMerge(t *testing.T) {
s2.MicrosoftAuth.ClientId = "microsoft_test" s2.MicrosoftAuth.ClientId = "microsoft_test"
s2.SpotifyAuth.Enabled = true s2.SpotifyAuth.Enabled = true
s2.SpotifyAuth.ClientId = "spotify_test" s2.SpotifyAuth.ClientId = "spotify_test"
s2.KakaoAuth.Enabled = true
s2.KakaoAuth.ClientId = "kakao_test"
if err := s1.Merge(s2); err != nil { if err := s1.Merge(s2); err != nil {
t.Fatal(err) t.Fatal(err)
@ -182,6 +187,7 @@ func TestSettingsRedactClone(t *testing.T) {
s1.TwitterAuth.ClientSecret = "test123" s1.TwitterAuth.ClientSecret = "test123"
s1.MicrosoftAuth.ClientSecret = "test123" s1.MicrosoftAuth.ClientSecret = "test123"
s1.SpotifyAuth.ClientSecret = "test123" s1.SpotifyAuth.ClientSecret = "test123"
s1.KakaoAuth.ClientSecret = "test123"
s2, err := s1.RedactClone() s2, err := s1.RedactClone()
if err != nil { if err != nil {
@ -193,7 +199,7 @@ func TestSettingsRedactClone(t *testing.T) {
t.Fatal(err) 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}/_/#/auth/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}/_/#/auth/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}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"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},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"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}/_/#/auth/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}/_/#/auth/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}/_/#/auth/confirm-email-change/{TOKEN}"}},"logs":{"maxDays":5},"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},"recordAuthToken":{"secret":"******","duration":1209600},"recordPasswordResetToken":{"secret":"******","duration":1800},"recordEmailChangeToken":{"secret":"******","duration":1800},"recordVerificationToken":{"secret":"******","duration":604800},"emailAuth":{"enabled":false,"exceptDomains":null,"onlyDomains":null,"minPasswordLength":0},"googleAuth":{"enabled":false,"clientSecret":"******"},"facebookAuth":{"enabled":false,"clientSecret":"******"},"githubAuth":{"enabled":false,"clientSecret":"******"},"gitlabAuth":{"enabled":false,"clientSecret":"******"},"discordAuth":{"enabled":false,"clientSecret":"******"},"twitterAuth":{"enabled":false,"clientSecret":"******"},"microsoftAuth":{"enabled":false,"clientSecret":"******"},"spotifyAuth":{"enabled":false,"clientSecret":"******"},"kakaoAuth":{"enabled":false,"clientSecret":"******"}}`
if encodedStr := string(encoded); encodedStr != expected { if encodedStr := string(encoded); encodedStr != expected {
t.Fatalf("Expected\n%v\ngot\n%v", expected, encodedStr) t.Fatalf("Expected\n%v\ngot\n%v", expected, encodedStr)
@ -212,6 +218,7 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
s.TwitterAuth.ClientId = "twitter_test" s.TwitterAuth.ClientId = "twitter_test"
s.MicrosoftAuth.ClientId = "microsoft_test" s.MicrosoftAuth.ClientId = "microsoft_test"
s.SpotifyAuth.ClientId = "spotify_test" s.SpotifyAuth.ClientId = "spotify_test"
s.KakaoAuth.ClientId = "kakao_test"
result := s.NamedAuthProviderConfigs() result := s.NamedAuthProviderConfigs()
@ -219,11 +226,23 @@ func TestNamedAuthProviderConfigs(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
encodedStr := string(encoded)
expected := `{"discord":{"enabled":false,"clientId":"discord_test"},"facebook":{"enabled":false,"clientId":"facebook_test"},"github":{"enabled":false,"clientId":"github_test"},"gitlab":{"enabled":true,"clientId":"gitlab_test"},"google":{"enabled":false,"clientId":"google_test"},"microsoft":{"enabled":false,"clientId":"microsoft_test"},"spotify":{"enabled":false,"clientId":"spotify_test"},"twitter":{"enabled":false,"clientId":"twitter_test"}}` expectedParts := []string{
`"discord":{"enabled":false,"clientId":"discord_test"}`,
if encodedStr := string(encoded); encodedStr != expected { `"facebook":{"enabled":false,"clientId":"facebook_test"}`,
t.Fatalf("Expected the same serialization, got \n%v", encodedStr) `"github":{"enabled":false,"clientId":"github_test"}`,
`"gitlab":{"enabled":true,"clientId":"gitlab_test"}`,
`"google":{"enabled":false,"clientId":"google_test"}`,
`"microsoft":{"enabled":false,"clientId":"microsoft_test"}`,
`"spotify":{"enabled":false,"clientId":"spotify_test"}`,
`"twitter":{"enabled":false,"clientId":"twitter_test"}`,
`"kakao":{"enabled":false,"clientId":"kakao_test"}`,
}
for _, p := range expectedParts {
if !strings.Contains(encodedStr, p) {
t.Fatalf("Expected \n%s \nin \n%s", p, encodedStr)
}
} }
} }

View File

@ -99,6 +99,8 @@ func NewProviderByName(name string) (Provider, error) {
return NewMicrosoftProvider(), nil return NewMicrosoftProvider(), nil
case NameSpotify: case NameSpotify:
return NewSpotifyProvider(), nil return NewSpotifyProvider(), nil
case NameKakao:
return NewKakaoProvider(), nil
default: default:
return nil, errors.New("Missing provider " + name) return nil, errors.New("Missing provider " + name)
} }

View File

@ -55,6 +55,15 @@ func TestNewProviderByName(t *testing.T) {
t.Error("Expected to be instance of *auth.Gitlab") 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 // discord
p, err = auth.NewProviderByName(auth.NameDiscord) p, err = auth.NewProviderByName(auth.NameDiscord)
if err != nil { if err != nil {
@ -81,4 +90,13 @@ func TestNewProviderByName(t *testing.T) {
if _, ok := p.(*auth.Spotify); !ok { if _, ok := p.(*auth.Spotify); !ok {
t.Error("Expected to be instance of *auth.Spotify") 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")
}
} }

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

@ -0,0 +1,60 @@
package auth
import (
"strconv"
"golang.org/x/oauth2"
"golang.org/x/oauth2/kakao"
)
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{
scopes: []string{"account_email", "profile_nickname", "profile_image"},
authUrl: kakao.Endpoint.AuthURL,
tokenUrl: kakao.Endpoint.TokenURL,
userApiUrl: "https://kapi.kakao.com/v2/user/me",
}}
}
// FetchAuthUser returns an AuthUser instance based on the Kakao's user api.
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 {
Id int `json:"id"`
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"`
}{}
if err := p.FetchRawUserData(token, &rawData); err != nil {
return nil, err
}
user := &AuthUser{
Id: strconv.Itoa(rawData.Id),
Username: rawData.Profile.Nickname,
AvatarUrl: rawData.Profile.ImageUrl,
}
if rawData.KakaoAccount.IsEmailValid && rawData.KakaoAccount.IsEmailVerified {
user.Email = rawData.KakaoAccount.Email
}
return user, nil
}

View File

@ -44,4 +44,8 @@ export default {
title: "Spotify", title: "Spotify",
icon: "ri-spotify-fill", icon: "ri-spotify-fill",
}, },
kakaoAuth: {
title: "Kakao",
icon: "ri-kakao-talk-fill",
},
}; };