Skip to content

Commit 3cc95b8

Browse files
committed
feat: add naver provider
1 parent 577e320 commit 3cc95b8

File tree

8 files changed

+312
-0
lines changed

8 files changed

+312
-0
lines changed

example.env

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ GOTRUE_EXTERNAL_KAKAO_CLIENT_ID=""
114114
GOTRUE_EXTERNAL_KAKAO_SECRET=""
115115
GOTRUE_EXTERNAL_KAKAO_REDIRECT_URI="http://localhost:9999/callback"
116116

117+
# Naver OAuth config
118+
GOTRUE_EXTERNAL_NAVER_ENABLED="false"
119+
GOTRUE_EXTERNAL_NAVER_CLIENT_ID=""
120+
GOTRUE_EXTERNAL_NAVER_SECRET=""
121+
GOTRUE_EXTERNAL_NAVER_REDIRECT_URI="http://localhost:9999/callback"
122+
117123
# Notion OAuth config
118124
GOTRUE_EXTERNAL_NOTION_ENABLED="false"
119125
GOTRUE_EXTERNAL_NOTION_CLIENT_ID=""

hack/test.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
6464
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=testclientid
6565
GOTRUE_EXTERNAL_GOOGLE_SECRET=testsecret
6666
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=https://identity.services.netlify.com/callback
67+
GOTRUE_EXTERNAL_NAVER_ENABLED=true
68+
GOTRUE_EXTERNAL_NAVER_CLIENT_ID=testclientid
69+
GOTRUE_EXTERNAL_NAVER_SECRET=testsecret
70+
GOTRUE_EXTERNAL_NAVER_REDIRECT_URI=https://identity.services.netlify.com/callback
6771
GOTRUE_EXTERNAL_NOTION_ENABLED=true
6872
GOTRUE_EXTERNAL_NOTION_CLIENT_ID=testclientid
6973
GOTRUE_EXTERNAL_NOTION_SECRET=testsecret

internal/api/external.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide
540540
return provider.NewKeycloakProvider(config.External.Keycloak, scopes)
541541
case "linkedin":
542542
return provider.NewLinkedinProvider(config.External.Linkedin, scopes)
543+
case "naver":
544+
return provider.NewNaverProvider(config.External.Naver, scopes)
543545
case "notion":
544546
return provider.NewNotionProvider(config.External.Notion)
545547
case "spotify":
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httptest"
7+
"net/url"
8+
9+
jwt "github.com/golang-jwt/jwt"
10+
)
11+
12+
const (
13+
naverResponse string = `{"resultcode":"resultcode","message":"message","response":{"id":"123","nickname":"Naver Test","name":"Naver Test","email":"naver@example.com","gender":"gender","age":"age","birthday":"birthday","profile_image":"http://example.com/avatar","birthyear":"birthyear","mobile":"mobile"}}`
14+
naverResponseAnotherEmail string = `{"resultcode":"resultcode","message":"message","response":{"id":"123","nickname":"Naver Test","name":"Naver Test","email":"another@example.com","gender":"gender","age":"age","birthday":"birthday","profile_image":"http://example.com/avatar","birthyear":"birthyear","mobile":"mobile"}}`
15+
naverResponseNoEmail string = `{"resultcode":"resultcode","message":"message","response":{"id":"123","nickname":"Naver Test","name":"Naver Test","gender":"gender","age":"age","birthday":"birthday","profile_image":"http://example.com/avatar","birthyear":"birthyear","mobile":"mobile"}}`
16+
)
17+
18+
func (ts *ExternalTestSuite) TestSignupExternalNaver() {
19+
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=naver", nil)
20+
w := httptest.NewRecorder()
21+
ts.API.handler.ServeHTTP(w, req)
22+
ts.Require().Equal(http.StatusFound, w.Code)
23+
u, err := url.Parse(w.Header().Get("Location"))
24+
ts.Require().NoError(err, "redirect url parse failed")
25+
q := u.Query()
26+
ts.Equal(ts.Config.External.Naver.RedirectURI, q.Get("redirect_uri"))
27+
ts.Equal(ts.Config.External.Naver.ClientID, []string{q.Get("client_id")})
28+
ts.Equal("code", q.Get("response_type"))
29+
30+
claims := ExternalProviderClaims{}
31+
p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
32+
_, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) {
33+
return []byte(ts.Config.JWT.Secret), nil
34+
})
35+
ts.Require().NoError(err)
36+
37+
ts.Equal("naver", claims.Provider)
38+
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
39+
}
40+
41+
func NaverTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, userCount *int, code string, response string) *httptest.Server {
42+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43+
switch r.URL.Path {
44+
case "/oauth2.0/token":
45+
*tokenCount++
46+
ts.Equal(code, r.FormValue("code"))
47+
ts.Equal("authorization_code", r.FormValue("grant_type"))
48+
ts.Equal(ts.Config.External.Naver.RedirectURI, r.FormValue("redirect_uri"))
49+
w.Header().Add("Content-Type", "application/json")
50+
fmt.Fprint(w, `{"access_token":"naver_token","expires_in":100000}`)
51+
case "/v1/nid/me":
52+
*userCount++
53+
w.Header().Add("Content-Type", "application/json")
54+
fmt.Fprint(w, response)
55+
default:
56+
w.WriteHeader(500)
57+
ts.Fail("unknown naver oauth call %s", r.URL.Path)
58+
}
59+
}))
60+
ts.Config.External.Naver.URL = server.URL
61+
return server
62+
}
63+
64+
func (ts *ExternalTestSuite) TestSignupExternalNaver_AuthorizationCode() {
65+
tokenCount, userCount := 0, 0
66+
code := "authcode"
67+
response := naverResponse
68+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
69+
defer server.Close()
70+
u := performAuthorization(ts, "naver", code, "")
71+
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "naver@example.com", "Naver Test", "123", "http://example.com/avatar")
72+
}
73+
74+
func (ts *ExternalTestSuite) TestSignupExternalNaverDisableSignupErrorWhenNoUser() {
75+
ts.Config.DisableSignup = true
76+
tokenCount, userCount := 0, 0
77+
code := "authcode"
78+
response := naverResponse
79+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
80+
defer server.Close()
81+
82+
u := performAuthorization(ts, "naver", code, "")
83+
84+
assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "naver@example.com")
85+
}
86+
87+
func (ts *ExternalTestSuite) TestSignupExternalNaverDisableSignupErrorWhenEmptyEmail() {
88+
ts.Config.DisableSignup = true
89+
tokenCount, userCount := 0, 0
90+
code := "authcode"
91+
response := naverResponseNoEmail
92+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
93+
defer server.Close()
94+
95+
u := performAuthorization(ts, "naver", code, "")
96+
97+
assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "naver@example.com")
98+
}
99+
100+
func (ts *ExternalTestSuite) TestSignupExternalNaverDisableSignupSuccessWithPrimaryEmail() {
101+
ts.Config.DisableSignup = true
102+
103+
ts.createUser("123", "naver@example.com", "Naver Test", "http://example.com/avatar", "")
104+
105+
tokenCount, userCount := 0, 0
106+
code := "authcode"
107+
response := naverResponse
108+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
109+
defer server.Close()
110+
111+
u := performAuthorization(ts, "naver", code, "")
112+
113+
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "naver@example.com", "Naver Test", "123", "http://example.com/avatar")
114+
}
115+
116+
func (ts *ExternalTestSuite) TestInviteTokenExternalNaverSuccessWhenMatchingToken() {
117+
// name and avatar should be populated from Naver API
118+
ts.createUser("123", "naver@example.com", "", "", "invite_token")
119+
120+
tokenCount, userCount := 0, 0
121+
code := "authcode"
122+
response := naverResponse
123+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
124+
defer server.Close()
125+
126+
u := performAuthorization(ts, "naver", code, "invite_token")
127+
128+
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "naver@example.com", "Naver Test", "123", "http://example.com/avatar")
129+
}
130+
131+
func (ts *ExternalTestSuite) TestInviteTokenExternalNaverErrorWhenNoMatchingToken() {
132+
tokenCount, userCount := 0, 0
133+
code := "authcode"
134+
response := naverResponse
135+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
136+
defer server.Close()
137+
138+
w := performAuthorizationRequest(ts, "naver", "invite_token")
139+
ts.Require().Equal(http.StatusNotFound, w.Code)
140+
}
141+
142+
func (ts *ExternalTestSuite) TestInviteTokenExternalNaverErrorWhenWrongToken() {
143+
ts.createUser("123", "naver@example.com", "", "", "invite_token")
144+
145+
tokenCount, userCount := 0, 0
146+
code := "authcode"
147+
response := naverResponse
148+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
149+
defer server.Close()
150+
151+
w := performAuthorizationRequest(ts, "naver", "wrong_token")
152+
ts.Require().Equal(http.StatusNotFound, w.Code)
153+
}
154+
155+
func (ts *ExternalTestSuite) TestInviteTokenExternalNaverErrorWhenEmailDoesntMatch() {
156+
ts.createUser("123", "naver@example.com", "", "", "invite_token")
157+
158+
tokenCount, userCount := 0, 0
159+
code := "authcode"
160+
response := naverResponseAnotherEmail
161+
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, response)
162+
defer server.Close()
163+
164+
u := performAuthorization(ts, "naver", code, "invite_token")
165+
166+
assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
167+
}
168+
169+
// func (ts *ExternalTestSuite) TestSignupExternalNaverErrorWhenVerifiedFalse() {
170+
// tokenCount, userCount := 0, 0
171+
// code := "authcode"
172+
// emails := `[{"email":"naver@example.com", "primary": true, "verified": false}]`
173+
// server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
174+
// defer server.Close()
175+
176+
// u := performAuthorization(ts, "naver", code, "")
177+
178+
// v, err := url.ParseQuery(u.Fragment)
179+
// ts.Require().NoError(err)
180+
// ts.Equal("unauthorized_client", v.Get("error"))
181+
// ts.Equal("401", v.Get("error_code"))
182+
// ts.Equal("Unverified email with naver", v.Get("error_description"))
183+
// assertAuthorizationFailure(ts, u, "", "", "")
184+
// }

internal/api/provider/naver.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"errors"
6+
"strings"
7+
8+
"github.com/supabase/gotrue/internal/conf"
9+
"golang.org/x/oauth2"
10+
)
11+
12+
const (
13+
defaultNaverAuthBase = "nid.naver.com"
14+
defaultNaverAPIBase = "openapi.naver.com"
15+
)
16+
17+
type naverProvider struct {
18+
*oauth2.Config
19+
APIHost string
20+
}
21+
22+
type naverResponse struct {
23+
Resultcode string `json:"resultcode"`
24+
Message string `json:"message"`
25+
Response struct {
26+
ID string `json:"id"`
27+
Nickname string `json:"nickname"`
28+
Name string `json:"name"`
29+
Email string `json:"email"`
30+
Gender string `json:"gender"`
31+
Age string `json:"age"`
32+
Birthday string `json:"birthday"`
33+
ProfileImage string `json:"profile_image"`
34+
Birthyear string `json:"birthyear"`
35+
Mobile string `json:"mobile"`
36+
} `json:"response"`
37+
}
38+
39+
func (p naverProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
40+
return p.Exchange(context.Background(), code)
41+
}
42+
43+
func (p naverProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
44+
var r naverResponse
45+
46+
if err := makeRequest(ctx, tok, p.Config, p.APIHost+"/v1/nid/me", &r); err != nil {
47+
return nil, err
48+
}
49+
50+
if r.Response.Email == "" {
51+
return nil, errors.New("unable to find email with Naver provider")
52+
}
53+
54+
data := &UserProvidedData{
55+
Emails: []Email{
56+
{
57+
Email: r.Response.Email,
58+
Verified: true, // Naver dosen't provide data on if email is verified.
59+
Primary: true,
60+
},
61+
},
62+
Metadata: &Claims{
63+
Issuer: p.APIHost,
64+
Subject: r.Response.ID,
65+
Email: r.Response.Email,
66+
EmailVerified: true, // Naver dosen't provide data on if email is verified.
67+
68+
Name: r.Response.Name,
69+
PreferredUsername: r.Response.Name,
70+
71+
// To be deprecated
72+
AvatarURL: r.Response.ProfileImage,
73+
FullName: r.Response.Name,
74+
ProviderId: r.Response.ID,
75+
UserNameKey: r.Response.Name,
76+
},
77+
}
78+
return data, nil
79+
}
80+
81+
func NewNaverProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
82+
if err := ext.ValidateOAuth(); err != nil {
83+
return nil, err
84+
}
85+
86+
authHost := chooseHost(ext.URL, defaultNaverAuthBase)
87+
apiHost := chooseHost(ext.URL, defaultNaverAPIBase)
88+
89+
oauthScopes := []string{
90+
"email",
91+
"profile_image",
92+
}
93+
94+
if scopes != "" {
95+
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
96+
}
97+
98+
return &naverProvider{
99+
Config: &oauth2.Config{
100+
ClientID: ext.ClientID[0],
101+
ClientSecret: ext.Secret,
102+
Endpoint: oauth2.Endpoint{
103+
AuthStyle: oauth2.AuthStyleInParams,
104+
AuthURL: authHost + "/oauth2.0/authorize",
105+
TokenURL: authHost + "/oauth2.0/token",
106+
},
107+
RedirectURL: ext.RedirectURI,
108+
Scopes: oauthScopes,
109+
},
110+
APIHost: apiHost,
111+
}, nil
112+
}

internal/api/settings.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type ProviderSettings struct {
1515
Keycloak bool `json:"keycloak"`
1616
Kakao bool `json:"kakao"`
1717
Linkedin bool `json:"linkedin"`
18+
Naver bool `json:"naver"`
1819
Notion bool `json:"notion"`
1920
Spotify bool `json:"spotify"`
2021
Slack bool `json:"slack"`
@@ -53,6 +54,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
5354
Kakao: config.External.Kakao.Enabled,
5455
Keycloak: config.External.Keycloak.Enabled,
5556
Linkedin: config.External.Linkedin.Enabled,
57+
Naver: config.External.Naver.Enabled,
5658
Notion: config.External.Notion.Enabled,
5759
Spotify: config.External.Spotify.Enabled,
5860
Slack: config.External.Slack.Enabled,

internal/api/settings_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func TestSettings_DefaultProviders(t *testing.T) {
4444
require.True(t, p.Twitch)
4545
require.True(t, p.WorkOS)
4646
require.True(t, p.Zoom)
47+
require.True(t, p.Naver)
4748
}
4849

4950
func TestSettings_EmailDisabled(t *testing.T) {

internal/conf/configuration.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ type ProviderConfiguration struct {
193193
Gitlab OAuthProviderConfiguration `json:"gitlab"`
194194
Google OAuthProviderConfiguration `json:"google"`
195195
Kakao OAuthProviderConfiguration `json:"kakao"`
196+
Naver OAuthProviderConfiguration `json:"naver"`
196197
Notion OAuthProviderConfiguration `json:"notion"`
197198
Keycloak OAuthProviderConfiguration `json:"keycloak"`
198199
Linkedin OAuthProviderConfiguration `json:"linkedin"`

0 commit comments

Comments
 (0)