From 51d040776a8ba4768e412da18ddbfbd941a4a896 Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Sun, 27 Nov 2022 23:23:03 +0900 Subject: [PATCH 01/15] feat(kakao):add Kakao Provider --- api/external.go | 2 + api/provider/kakao.go | 95 +++++++++++++++++++++++++++++++++++++++++++ api/settings.go | 2 + api/settings_test.go | 1 + conf/configuration.go | 1 + example.env | 6 +++ 6 files changed, 107 insertions(+) create mode 100644 api/provider/kakao.go diff --git a/api/external.go b/api/external.go index c9ea04553..384a461a3 100644 --- a/api/external.go +++ b/api/external.go @@ -460,6 +460,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide return provider.NewGitlabProvider(config.External.Gitlab, scopes) case "google": return provider.NewGoogleProvider(config.External.Google, scopes) + case "kakao": + return provider.NewKakaoProvider(config.External.Kakao, scopes) case "keycloak": return provider.NewKeycloakProvider(config.External.Keycloak, scopes) case "linkedin": diff --git a/api/provider/kakao.go b/api/provider/kakao.go new file mode 100644 index 000000000..b98d4aec0 --- /dev/null +++ b/api/provider/kakao.go @@ -0,0 +1,95 @@ +package provider + +import ( + "context" + "fmt" + "strconv" + + "github.com/netlify/gotrue/conf" + "golang.org/x/oauth2" +) + +const ( + defaultKakaoAuthBase = "kauth.kakao.com" + defaultKakaoAPIBase = "kapi.kakao.com" +) + +type kakaoProvider struct { + *oauth2.Config + APIHost string +} + +type kakaoUser struct { + ID int `json:"id"` + Account struct { + Profile struct { + Name string `json:"nickname"` + ProfileImageURL string `json:"profile_image_url"` + } `json:"profile"` + Email string `json:"email"` + EmailValid bool `json:"is_email_valid"` + EmailVerified bool `json:"is_email_verified"` + } `json:"kakao_account"` +} + +func (p kakaoProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + fmt.Printf("GetOAuthToken: %v\n", code) + return p.Exchange(context.Background(), code) +} + +func (p kakaoProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { + var u kakaoUser + + if err := makeRequest(ctx, tok, p.Config, p.APIHost+"/v2/user/me", &u); err != nil { + return nil, err + } + + data := &UserProvidedData{ + Emails: []Email{ + { + Email: u.Account.Email, + Verified: u.Account.EmailVerified && u.Account.EmailValid, + Primary: true, + }, + }, + Metadata: &Claims{ + Issuer: p.APIHost, + Subject: strconv.Itoa(u.ID), + Name: u.Account.Profile.Name, + PreferredUsername: u.Account.Profile.Name, + + // To be deprecated + AvatarURL: u.Account.Profile.ProfileImageURL, + FullName: u.Account.Profile.Name, + ProviderId: strconv.Itoa(u.ID), + UserNameKey: u.Account.Profile.Name, + }, + } + return data, nil +} + +func NewKakaoProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { + if err := ext.Validate(); err != nil { + return nil, err + } + + authHost := chooseHost(ext.URL, defaultKakaoAuthBase) + apiHost := chooseHost(ext.URL, defaultKakaoAPIBase) + + oauthScopes := []string{} + + return &kakaoProvider{ + Config: &oauth2.Config{ + ClientID: ext.ClientID, + ClientSecret: ext.Secret, + Endpoint: oauth2.Endpoint{ + AuthStyle: oauth2.AuthStyleInParams, + AuthURL: authHost + "/oauth/authorize", + TokenURL: authHost + "/oauth/token", + }, + RedirectURL: ext.RedirectURI, + Scopes: oauthScopes, + }, + APIHost: apiHost, + }, nil +} diff --git a/api/settings.go b/api/settings.go index bf840b2e1..4fbb50367 100644 --- a/api/settings.go +++ b/api/settings.go @@ -11,6 +11,7 @@ type ProviderSettings struct { GitLab bool `json:"gitlab"` Keycloak bool `json:"keycloak"` Google bool `json:"google"` + Kakao bool `json:"kakao"` Linkedin bool `json:"linkedin"` Facebook bool `json:"facebook"` Notion bool `json:"notion"` @@ -46,6 +47,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error { GitHub: config.External.Github.Enabled, GitLab: config.External.Gitlab.Enabled, Google: config.External.Google.Enabled, + Kakao: config.External.Kakao.Enabled, Keycloak: config.External.Keycloak.Enabled, Linkedin: config.External.Linkedin.Enabled, Facebook: config.External.Facebook.Enabled, diff --git a/api/settings_test.go b/api/settings_test.go index 781fce469..2f3a6ef6f 100644 --- a/api/settings_test.go +++ b/api/settings_test.go @@ -39,6 +39,7 @@ func TestSettings_DefaultProviders(t *testing.T) { require.True(t, p.Spotify) require.True(t, p.Slack) require.True(t, p.Google) + require.True(t, p.Kakao) require.True(t, p.Keycloak) require.True(t, p.Linkedin) require.True(t, p.GitHub) diff --git a/conf/configuration.go b/conf/configuration.go index ad5822c56..3698c507c 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -142,6 +142,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` + Kakao OAuthProviderConfiguration `json:"kakao"` Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` diff --git a/example.env b/example.env index a6c330e0e..8eabc032c 100644 --- a/example.env +++ b/example.env @@ -96,6 +96,12 @@ GOTRUE_EXTERNAL_GITHUB_CLIENT_ID="" GOTRUE_EXTERNAL_GITHUB_SECRET="" GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI="http://localhost:9999/callback" +# Kakoo OAuth config +GOTRUE_EXTERNAL_KAKAO_ENABLED="false" +GOTRUE_EXTERNAL_KAKAO_CLIENT_ID="" +GOTRUE_EXTERNAL_KAKAO_SECRET="" +GOTRUE_EXTERNAL_KAKAO_REDIRECT_URI="http://localhost:9999/callback" + # Facebook OAuth config GOTRUE_EXTERNAL_FACEBOOK_ENABLED="false" GOTRUE_EXTERNAL_FACEBOOK_CLIENT_ID="" From 689da5ff70546f7c7498150928c6a61acb358d51 Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Mon, 28 Nov 2022 11:17:27 +0900 Subject: [PATCH 02/15] fix: typo in env --- example.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.env b/example.env index 8eabc032c..bc61277de 100644 --- a/example.env +++ b/example.env @@ -96,7 +96,7 @@ GOTRUE_EXTERNAL_GITHUB_CLIENT_ID="" GOTRUE_EXTERNAL_GITHUB_SECRET="" GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI="http://localhost:9999/callback" -# Kakoo OAuth config +# Kakao OAuth config GOTRUE_EXTERNAL_KAKAO_ENABLED="false" GOTRUE_EXTERNAL_KAKAO_CLIENT_ID="" GOTRUE_EXTERNAL_KAKAO_SECRET="" From c78c965c4689d57da01a6f6b4f158a0b42e2534a Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Mon, 28 Nov 2022 11:19:37 +0900 Subject: [PATCH 03/15] fix(kakao): remove unused logs --- api/provider/kakao.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/provider/kakao.go b/api/provider/kakao.go index b98d4aec0..484b9325b 100644 --- a/api/provider/kakao.go +++ b/api/provider/kakao.go @@ -2,7 +2,6 @@ package provider import ( "context" - "fmt" "strconv" "github.com/netlify/gotrue/conf" @@ -33,7 +32,6 @@ type kakaoUser struct { } func (p kakaoProvider) GetOAuthToken(code string) (*oauth2.Token, error) { - fmt.Printf("GetOAuthToken: %v\n", code) return p.Exchange(context.Background(), code) } From 9ba5ad971104e893a1ede5d3acc45d7d0a4639df Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Mon, 28 Nov 2022 11:20:00 +0900 Subject: [PATCH 04/15] feat(kakao): add kakao test env --- hack/test.env | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hack/test.env b/hack/test.env index 74bcd04bc..9b4d31cd5 100644 --- a/hack/test.env +++ b/hack/test.env @@ -39,6 +39,10 @@ GOTRUE_EXTERNAL_GITHUB_ENABLED=true GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=testclientid GOTRUE_EXTERNAL_GITHUB_SECRET=testsecret GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI=https://identity.services.netlify.com/callback +GOTRUE_EXTERNAL_KAKAO_ENABLED=true +GOTRUE_EXTERNAL_KAKAO_CLIENT_ID=testclientid +GOTRUE_EXTERNAL_KAKAO_SECRET=testsecret +GOTRUE_EXTERNAL_KAKAO_REDIRECT_URI=https://identity.services.netlify.com/callback GOTRUE_EXTERNAL_KEYCLOAK_ENABLED=true GOTRUE_EXTERNAL_KEYCLOAK_CLIENT_ID=testclientid GOTRUE_EXTERNAL_KEYCLOAK_SECRET=testsecret From 593256705da4c7fc173351cbbe3d4e6c295ed88f Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Tue, 29 Nov 2022 11:07:04 +0900 Subject: [PATCH 05/15] feat(kakao): add kakao tests --- api/external_kakao_test.go | 234 +++++++++++++++++++++++++++++++++++++ api/provider/kakao.go | 7 +- 2 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 api/external_kakao_test.go diff --git a/api/external_kakao_test.go b/api/external_kakao_test.go new file mode 100644 index 000000000..676333308 --- /dev/null +++ b/api/external_kakao_test.go @@ -0,0 +1,234 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "time" + + jwt "github.com/golang-jwt/jwt" + "github.com/netlify/gotrue/api/provider" + "github.com/netlify/gotrue/models" + "github.com/stretchr/testify/require" +) + +func (ts *ExternalTestSuite) TestSignupExternalKakao() { + req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=kakao", nil) + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + ts.Require().Equal(http.StatusFound, w.Code) + u, err := url.Parse(w.Header().Get("Location")) + ts.Require().NoError(err, "redirect url parse failed") + q := u.Query() + ts.Equal(ts.Config.External.Kakao.RedirectURI, q.Get("redirect_uri")) + ts.Equal(ts.Config.External.Kakao.ClientID, q.Get("client_id")) + ts.Equal("code", q.Get("response_type")) + + claims := ExternalProviderClaims{} + p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} + _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { + return []byte(ts.Config.JWT.Secret), nil + }) + ts.Require().NoError(err) + + ts.Equal("kakao", claims.Provider) + ts.Equal(ts.Config.SiteURL, claims.SiteURL) +} + +func KakaoTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, emails string) *httptest.Server { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth/token": + *tokenCount++ + ts.Equal(code, r.FormValue("code")) + ts.Equal("authorization_code", r.FormValue("grant_type")) + ts.Equal(ts.Config.External.Kakao.RedirectURI, r.FormValue("redirect_uri")) + w.Header().Add("Content-Type", "application/json") + fmt.Fprint(w, `{"access_token":"kakao_token","expires_in":100000}`) + case "/v2/user/me": + *userCount++ + var emailList []provider.Email + if err := json.Unmarshal([]byte(emails), &emailList); err != nil { + ts.Fail("Invalid email json %s", emails) + } + + var email *provider.Email + + for i, e := range emailList { + if len(e.Email) > 0 { + email = &emailList[i] + break + } + } + + if email == nil { + w.WriteHeader(400) + return + } + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "id":123, + "kakao_account": { + "profile": { + "nickname":"Kakao Test", + "profile_image_url":"http://example.com/avatar" + }, + "email": "%v", + "is_email_valid": %v, + "is_email_verified": %v + } + }`, email.Email, email.Verified, email.Verified) + default: + w.WriteHeader(500) + ts.Fail("unknown kakao oauth call %s", r.URL.Path) + } + })) + ts.Config.External.Kakao.URL = server.URL + return server +} + +func (ts *ExternalTestSuite) TestSignupExternalKakao_AuthorizationCode() { + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + u := performAuthorization(ts, "kakao", code, "") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupErrorWhenNoUser() { + ts.Config.DisableSignup = true + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + u := performAuthorization(ts, "kakao", code, "") + + assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "kakao@example.com") +} + +func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupErrorWhenEmptyEmail() { + ts.Config.DisableSignup = true + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + u := performAuthorization(ts, "kakao", code, "") + + assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "kakao@example.com") +} + +func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupSuccessWithPrimaryEmail() { + ts.Config.DisableSignup = true + + ts.createUser("123", "kakao@example.com", "Kakao Test", "http://example.com/avatar", "") + + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + u := performAuthorization(ts, "kakao", code, "") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoSuccessWhenMatchingToken() { + // name and avatar should be populated from Kakao API + ts.createUser("123", "kakao@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + u := performAuthorization(ts, "kakao", code, "invite_token") + + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenNoMatchingToken() { + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + w := performAuthorizationRequest(ts, "kakao", "invite_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenWrongToken() { + ts.createUser("123", "kakao@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + w := performAuthorizationRequest(ts, "kakao", "wrong_token") + ts.Require().Equal(http.StatusNotFound, w.Code) +} + +func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenEmailDoesntMatch() { + ts.createUser("123", "kakao@example.com", "", "", "invite_token") + + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"other@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + u := performAuthorization(ts, "kakao", code, "invite_token") + + assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "") +} + +func (ts *ExternalTestSuite) TestSignupExternalKakaoErrorWhenVerifiedFalse() { + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": false}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + u := performAuthorization(ts, "kakao", code, "") + + v, err := url.ParseQuery(u.Fragment) + ts.Require().NoError(err) + ts.Equal("unauthorized_client", v.Get("error")) + ts.Equal("401", v.Get("error_code")) + ts.Equal("Unverified email with kakao", v.Get("error_description")) + assertAuthorizationFailure(ts, u, "", "", "") +} + +func (ts *ExternalTestSuite) TestSignupExternalKakaoErrorWhenUserBanned() { + tokenCount, userCount := 0, 0 + code := "authcode" + emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` + server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) + defer server.Close() + + u := performAuthorization(ts, "kakao", code, "") + assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") + + user, err := models.FindUserByEmailAndAudience(ts.API.db, "kakao@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + t := time.Now().Add(24 * time.Hour) + user.BannedUntil = &t + require.NoError(ts.T(), ts.API.db.UpdateOnly(user, "banned_until")) + + u = performAuthorization(ts, "kakao", code, "") + assertAuthorizationFailure(ts, u, "User is unauthorized", "unauthorized_client", "") +} diff --git a/api/provider/kakao.go b/api/provider/kakao.go index 484b9325b..70e2b07e6 100644 --- a/api/provider/kakao.go +++ b/api/provider/kakao.go @@ -51,8 +51,11 @@ func (p kakaoProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*Use }, }, Metadata: &Claims{ - Issuer: p.APIHost, - Subject: strconv.Itoa(u.ID), + Issuer: p.APIHost, + Subject: strconv.Itoa(u.ID), + Email: u.Account.Email, + EmailVerified: u.Account.EmailVerified && u.Account.EmailValid, + Name: u.Account.Profile.Name, PreferredUsername: u.Account.Profile.Name, From 8798d6fba3e61db9af83b81232364dd1a9377d7d Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Fri, 27 Jan 2023 23:47:59 +0900 Subject: [PATCH 06/15] fix(scope): Kakao OAuth scope --- api/provider/kakao.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/api/provider/kakao.go b/api/provider/kakao.go index 70e2b07e6..f19d64cff 100644 --- a/api/provider/kakao.go +++ b/api/provider/kakao.go @@ -3,6 +3,7 @@ package provider import ( "context" "strconv" + "strings" "github.com/netlify/gotrue/conf" "golang.org/x/oauth2" @@ -77,7 +78,15 @@ func NewKakaoProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuth authHost := chooseHost(ext.URL, defaultKakaoAuthBase) apiHost := chooseHost(ext.URL, defaultKakaoAPIBase) - oauthScopes := []string{} + oauthScopes := []string{ + "account_email", + "profile_image", + "profile_nickname" , + } + + if scopes != "" { + oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) + } return &kakaoProvider{ Config: &oauth2.Config{ From 2db0fa007e6efff3d49ee00ec3c62baae31b6d0c Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Sat, 28 Jan 2023 01:00:45 +0900 Subject: [PATCH 07/15] fix(typo): remove extra space --- api/provider/kakao.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/provider/kakao.go b/api/provider/kakao.go index f19d64cff..0c6347f01 100644 --- a/api/provider/kakao.go +++ b/api/provider/kakao.go @@ -81,7 +81,7 @@ func NewKakaoProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuth oauthScopes := []string{ "account_email", "profile_image", - "profile_nickname" , + "profile_nickname", } if scopes != "" { From b13d54634c32232cd6818b57afa19547c335455d Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Thu, 9 Feb 2023 15:32:10 +0900 Subject: [PATCH 08/15] fix(convention): remove kakao api from /api (using internal/api instead) --- api/external_kakao_test.go | 234 ------------------------------------- api/provider/kakao.go | 105 ----------------- 2 files changed, 339 deletions(-) delete mode 100644 api/external_kakao_test.go delete mode 100644 api/provider/kakao.go diff --git a/api/external_kakao_test.go b/api/external_kakao_test.go deleted file mode 100644 index 676333308..000000000 --- a/api/external_kakao_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "time" - - jwt "github.com/golang-jwt/jwt" - "github.com/netlify/gotrue/api/provider" - "github.com/netlify/gotrue/models" - "github.com/stretchr/testify/require" -) - -func (ts *ExternalTestSuite) TestSignupExternalKakao() { - req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=kakao", nil) - w := httptest.NewRecorder() - ts.API.handler.ServeHTTP(w, req) - ts.Require().Equal(http.StatusFound, w.Code) - u, err := url.Parse(w.Header().Get("Location")) - ts.Require().NoError(err, "redirect url parse failed") - q := u.Query() - ts.Equal(ts.Config.External.Kakao.RedirectURI, q.Get("redirect_uri")) - ts.Equal(ts.Config.External.Kakao.ClientID, q.Get("client_id")) - ts.Equal("code", q.Get("response_type")) - - claims := ExternalProviderClaims{} - p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} - _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { - return []byte(ts.Config.JWT.Secret), nil - }) - ts.Require().NoError(err) - - ts.Equal("kakao", claims.Provider) - ts.Equal(ts.Config.SiteURL, claims.SiteURL) -} - -func KakaoTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, emails string) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/oauth/token": - *tokenCount++ - ts.Equal(code, r.FormValue("code")) - ts.Equal("authorization_code", r.FormValue("grant_type")) - ts.Equal(ts.Config.External.Kakao.RedirectURI, r.FormValue("redirect_uri")) - w.Header().Add("Content-Type", "application/json") - fmt.Fprint(w, `{"access_token":"kakao_token","expires_in":100000}`) - case "/v2/user/me": - *userCount++ - var emailList []provider.Email - if err := json.Unmarshal([]byte(emails), &emailList); err != nil { - ts.Fail("Invalid email json %s", emails) - } - - var email *provider.Email - - for i, e := range emailList { - if len(e.Email) > 0 { - email = &emailList[i] - break - } - } - - if email == nil { - w.WriteHeader(400) - return - } - - w.Header().Add("Content-Type", "application/json") - fmt.Fprintf(w, ` - { - "id":123, - "kakao_account": { - "profile": { - "nickname":"Kakao Test", - "profile_image_url":"http://example.com/avatar" - }, - "email": "%v", - "is_email_valid": %v, - "is_email_verified": %v - } - }`, email.Email, email.Verified, email.Verified) - default: - w.WriteHeader(500) - ts.Fail("unknown kakao oauth call %s", r.URL.Path) - } - })) - ts.Config.External.Kakao.URL = server.URL - return server -} - -func (ts *ExternalTestSuite) TestSignupExternalKakao_AuthorizationCode() { - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - u := performAuthorization(ts, "kakao", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") -} - -func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupErrorWhenNoUser() { - ts.Config.DisableSignup = true - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - u := performAuthorization(ts, "kakao", code, "") - - assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "kakao@example.com") -} - -func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupErrorWhenEmptyEmail() { - ts.Config.DisableSignup = true - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - u := performAuthorization(ts, "kakao", code, "") - - assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "kakao@example.com") -} - -func (ts *ExternalTestSuite) TestSignupExternalKakaoDisableSignupSuccessWithPrimaryEmail() { - ts.Config.DisableSignup = true - - ts.createUser("123", "kakao@example.com", "Kakao Test", "http://example.com/avatar", "") - - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - u := performAuthorization(ts, "kakao", code, "") - - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") -} - -func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoSuccessWhenMatchingToken() { - // name and avatar should be populated from Kakao API - ts.createUser("123", "kakao@example.com", "", "", "invite_token") - - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - u := performAuthorization(ts, "kakao", code, "invite_token") - - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") -} - -func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenNoMatchingToken() { - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - w := performAuthorizationRequest(ts, "kakao", "invite_token") - ts.Require().Equal(http.StatusNotFound, w.Code) -} - -func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenWrongToken() { - ts.createUser("123", "kakao@example.com", "", "", "invite_token") - - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - w := performAuthorizationRequest(ts, "kakao", "wrong_token") - ts.Require().Equal(http.StatusNotFound, w.Code) -} - -func (ts *ExternalTestSuite) TestInviteTokenExternalKakaoErrorWhenEmailDoesntMatch() { - ts.createUser("123", "kakao@example.com", "", "", "invite_token") - - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"other@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - u := performAuthorization(ts, "kakao", code, "invite_token") - - assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "") -} - -func (ts *ExternalTestSuite) TestSignupExternalKakaoErrorWhenVerifiedFalse() { - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": false}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - u := performAuthorization(ts, "kakao", code, "") - - v, err := url.ParseQuery(u.Fragment) - ts.Require().NoError(err) - ts.Equal("unauthorized_client", v.Get("error")) - ts.Equal("401", v.Get("error_code")) - ts.Equal("Unverified email with kakao", v.Get("error_description")) - assertAuthorizationFailure(ts, u, "", "", "") -} - -func (ts *ExternalTestSuite) TestSignupExternalKakaoErrorWhenUserBanned() { - tokenCount, userCount := 0, 0 - code := "authcode" - emails := `[{"email":"kakao@example.com", "primary": true, "verified": true}]` - server := KakaoTestSignupSetup(ts, &tokenCount, &userCount, code, emails) - defer server.Close() - - u := performAuthorization(ts, "kakao", code, "") - assertAuthorizationSuccess(ts, u, tokenCount, userCount, "kakao@example.com", "Kakao Test", "123", "http://example.com/avatar") - - user, err := models.FindUserByEmailAndAudience(ts.API.db, "kakao@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), err) - t := time.Now().Add(24 * time.Hour) - user.BannedUntil = &t - require.NoError(ts.T(), ts.API.db.UpdateOnly(user, "banned_until")) - - u = performAuthorization(ts, "kakao", code, "") - assertAuthorizationFailure(ts, u, "User is unauthorized", "unauthorized_client", "") -} diff --git a/api/provider/kakao.go b/api/provider/kakao.go deleted file mode 100644 index 0c6347f01..000000000 --- a/api/provider/kakao.go +++ /dev/null @@ -1,105 +0,0 @@ -package provider - -import ( - "context" - "strconv" - "strings" - - "github.com/netlify/gotrue/conf" - "golang.org/x/oauth2" -) - -const ( - defaultKakaoAuthBase = "kauth.kakao.com" - defaultKakaoAPIBase = "kapi.kakao.com" -) - -type kakaoProvider struct { - *oauth2.Config - APIHost string -} - -type kakaoUser struct { - ID int `json:"id"` - Account struct { - Profile struct { - Name string `json:"nickname"` - ProfileImageURL string `json:"profile_image_url"` - } `json:"profile"` - Email string `json:"email"` - EmailValid bool `json:"is_email_valid"` - EmailVerified bool `json:"is_email_verified"` - } `json:"kakao_account"` -} - -func (p kakaoProvider) GetOAuthToken(code string) (*oauth2.Token, error) { - return p.Exchange(context.Background(), code) -} - -func (p kakaoProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { - var u kakaoUser - - if err := makeRequest(ctx, tok, p.Config, p.APIHost+"/v2/user/me", &u); err != nil { - return nil, err - } - - data := &UserProvidedData{ - Emails: []Email{ - { - Email: u.Account.Email, - Verified: u.Account.EmailVerified && u.Account.EmailValid, - Primary: true, - }, - }, - Metadata: &Claims{ - Issuer: p.APIHost, - Subject: strconv.Itoa(u.ID), - Email: u.Account.Email, - EmailVerified: u.Account.EmailVerified && u.Account.EmailValid, - - Name: u.Account.Profile.Name, - PreferredUsername: u.Account.Profile.Name, - - // To be deprecated - AvatarURL: u.Account.Profile.ProfileImageURL, - FullName: u.Account.Profile.Name, - ProviderId: strconv.Itoa(u.ID), - UserNameKey: u.Account.Profile.Name, - }, - } - return data, nil -} - -func NewKakaoProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { - if err := ext.Validate(); err != nil { - return nil, err - } - - authHost := chooseHost(ext.URL, defaultKakaoAuthBase) - apiHost := chooseHost(ext.URL, defaultKakaoAPIBase) - - oauthScopes := []string{ - "account_email", - "profile_image", - "profile_nickname", - } - - if scopes != "" { - oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...) - } - - return &kakaoProvider{ - Config: &oauth2.Config{ - ClientID: ext.ClientID, - ClientSecret: ext.Secret, - Endpoint: oauth2.Endpoint{ - AuthStyle: oauth2.AuthStyleInParams, - AuthURL: authHost + "/oauth/authorize", - TokenURL: authHost + "/oauth/token", - }, - RedirectURL: ext.RedirectURI, - Scopes: oauthScopes, - }, - APIHost: apiHost, - }, nil -} From f32df7e0b0655a339eb9d476d5f26ad20a09c5bf Mon Sep 17 00:00:00 2001 From: Eunsoo Shin Date: Mon, 20 Feb 2023 14:54:08 +0900 Subject: [PATCH 09/15] fix(import): kakao imports --- internal/api/external_kakao_test.go | 4 ++-- internal/api/provider/kakao.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/api/external_kakao_test.go b/internal/api/external_kakao_test.go index eabbfbd95..7cfb51b38 100644 --- a/internal/api/external_kakao_test.go +++ b/internal/api/external_kakao_test.go @@ -9,9 +9,9 @@ import ( "time" jwt "github.com/golang-jwt/jwt" - "github.com/netlify/gotrue/internal/api/provider" - "github.com/netlify/gotrue/internal/models" "github.com/stretchr/testify/require" + "github.com/supabase/gotrue/internal/api/provider" + "github.com/supabase/gotrue/internal/models" ) func (ts *ExternalTestSuite) TestSignupExternalKakao() { diff --git a/internal/api/provider/kakao.go b/internal/api/provider/kakao.go index 684e09063..917facffe 100644 --- a/internal/api/provider/kakao.go +++ b/internal/api/provider/kakao.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/netlify/gotrue/internal/conf" + "github.com/supabase/gotrue/internal/conf" "golang.org/x/oauth2" ) From 6028053ffa93dad0ac56d4df7e275c824861fd7d Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 11 May 2023 23:37:20 +0800 Subject: [PATCH 10/15] Update internal/conf/configuration.go --- internal/conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 1e7280041..584e4a4f2 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -145,7 +145,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` - Kakao OAuthProviderConfiguration `json:"kakao"` + Kakao OAuthProviderConfiguration `json:"kakao"` Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` From 4a6bfeba1e74816358e832bc06867f4f34e1a06e Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 11 May 2023 23:37:49 +0800 Subject: [PATCH 11/15] Update internal/conf/configuration.go --- internal/conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 584e4a4f2..0c414ba2d 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -145,7 +145,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` - Kakao OAuthProviderConfiguration `json:"kakao"` + Kakao OAuthProviderConfiguration `json:"kakao"` Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` From dcbdc8995548e8b65bdbdf1a91d4a5834f28eeb0 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 11 May 2023 23:38:08 +0800 Subject: [PATCH 12/15] Update internal/conf/configuration.go --- internal/conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 0c414ba2d..f9890e955 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -145,7 +145,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` - Kakao OAuthProviderConfiguration `json:"kakao"` + Kakao OAuthProviderConfiguration `json:"kakao"` Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` From 3ea6fb16c3aa00c0bf2e6b1ebe3b2922638a399a Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 11 May 2023 23:41:21 +0800 Subject: [PATCH 13/15] Update internal/conf/configuration.go --- internal/conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index f9890e955..579789f6f 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -145,7 +145,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` - Kakao OAuthProviderConfiguration `json:"kakao"` + Kakao OAuthProviderConfiguration `json:"kakao"` Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` From 00a93ac5cd6840547100674169db33bbba6746f5 Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 11 May 2023 23:41:58 +0800 Subject: [PATCH 14/15] Update internal/conf/configuration.go --- internal/conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 579789f6f..838880abc 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -145,7 +145,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` - Kakao OAuthProviderConfiguration `json:"kakao"` + Kakao OAuthProviderConfiguration `json:"kakao"` Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"` From 157a7c9116dd31d90f2d9513f8f71e92733d91fd Mon Sep 17 00:00:00 2001 From: Kang Ming Date: Thu, 11 May 2023 23:48:34 +0800 Subject: [PATCH 15/15] fix fmt --- internal/conf/configuration.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index 838880abc..c6de717a9 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -145,7 +145,7 @@ type ProviderConfiguration struct { Github OAuthProviderConfiguration `json:"github"` Gitlab OAuthProviderConfiguration `json:"gitlab"` Google OAuthProviderConfiguration `json:"google"` - Kakao OAuthProviderConfiguration `json:"kakao"` + Kakao OAuthProviderConfiguration `json:"kakao"` Notion OAuthProviderConfiguration `json:"notion"` Keycloak OAuthProviderConfiguration `json:"keycloak"` Linkedin OAuthProviderConfiguration `json:"linkedin"`