Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ GOTRUE_EXTERNAL_KAKAO_CLIENT_ID=""
GOTRUE_EXTERNAL_KAKAO_SECRET=""
GOTRUE_EXTERNAL_KAKAO_REDIRECT_URI="http://localhost:9999/callback"

# Naver OAuth config
GOTRUE_EXTERNAL_NAVER_ENABLED="false"
GOTRUE_EXTERNAL_NAVER_CLIENT_ID=""
GOTRUE_EXTERNAL_NAVER_SECRET=""
GOTRUE_EXTERNAL_NAVER_REDIRECT_URI="http://localhost:9999/callback"

# Notion OAuth config
GOTRUE_EXTERNAL_NOTION_ENABLED="false"
GOTRUE_EXTERNAL_NOTION_CLIENT_ID=""
Expand Down
4 changes: 4 additions & 0 deletions hack/test.env
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ GOTRUE_EXTERNAL_GOOGLE_ENABLED=true
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_GOOGLE_SECRET=testsecret
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_NAVER_ENABLED=true
GOTRUE_EXTERNAL_NAVER_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_NAVER_SECRET=testsecret
GOTRUE_EXTERNAL_NAVER_REDIRECT_URI=https://identity.services.netlify.com/callback
GOTRUE_EXTERNAL_NOTION_ENABLED=true
GOTRUE_EXTERNAL_NOTION_CLIENT_ID=testclientid
GOTRUE_EXTERNAL_NOTION_SECRET=testsecret
Expand Down
2 changes: 2 additions & 0 deletions internal/api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide
return provider.NewGoogleProvider(ctx, config.External.Google, scopes)
case "kakao":
return provider.NewKakaoProvider(config.External.Kakao, scopes)
case "naver":
return provider.NewNaverProvider(config.External.Naver, scopes)
case "keycloak":
return provider.NewKeycloakProvider(config.External.Keycloak, scopes)
case "linkedin":
Expand Down
219 changes: 219 additions & 0 deletions internal/api/external_naver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package api

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"time"

jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"github.com/supabase/auth/internal/api/provider"
"github.com/supabase/auth/internal/models"
)

func (ts *ExternalTestSuite) TestSignupExternalNaver() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=naver", 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.Naver.RedirectURI, q.Get("redirect_uri"))
ts.Equal(ts.Config.External.Naver.ClientID, []string{q.Get("client_id")})
ts.Equal("code", q.Get("response_type"))

claims := ExternalProviderClaims{}
p := jwt.NewParser(jwt.WithValidMethods([]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("naver", claims.Provider)
ts.Equal(ts.Config.SiteURL, claims.SiteURL)
}

func NaverTestSignupSetup(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 "/oauth2.0/token":
*tokenCount++
ts.Equal(code, r.FormValue("code"))
ts.Equal("authorization_code", r.FormValue("grant_type"))
ts.Equal(ts.Config.External.Naver.RedirectURI, r.FormValue("redirect_uri"))
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, `{"access_token":"naver_token","expires_in":100000}`)
case "/v1/nid/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
}
}

w.Header().Add("Content-Type", "application/json")
if email != nil {
fmt.Fprintf(w, `
{
"response": {
"id": "123",
"name": "Naver Test",
"profile_image": "http://example.com/avatar",
"email": "%v"
}
}`, email.Email)
} else {
fmt.Fprint(w, `
{
"response": {
"id": "123",
"name": "Naver Test",
"profile_image": "http://example.com/avatar"
}
}`)
}
default:
w.WriteHeader(500)
ts.Fail("unknown naver oauth call %s", r.URL.Path)
}
}))
ts.Config.External.Naver.URL = server.URL
return server
}

func (ts *ExternalTestSuite) TestSignupExternalNaver_AuthorizationCode() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()
u := performAuthorization(ts, "naver", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "[email protected]", "Naver Test", "123", "http://example.com/avatar")
}

func (ts *ExternalTestSuite) TestSignupExternalNaverDisableSignupErrorWhenNoUser() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

u := performAuthorization(ts, "naver", code, "")

assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "[email protected]")
}

func (ts *ExternalTestSuite) TestSignupExternalNaverDisableSignupErrorWhenEmptyEmail() {
ts.Config.DisableSignup = true
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

u := performAuthorization(ts, "naver", code, "")

assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "[email protected]")
}

func (ts *ExternalTestSuite) TestSignupExternalNaverDisableSignupSuccessWithPrimaryEmail() {
ts.Config.DisableSignup = true

ts.createUser("123", "[email protected]", "Naver Test", "http://example.com/avatar", "")

tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

u := performAuthorization(ts, "naver", code, "")

assertAuthorizationSuccess(ts, u, tokenCount, userCount, "[email protected]", "Naver Test", "123", "http://example.com/avatar")
}

func (ts *ExternalTestSuite) TestInviteTokenExternalNaverSuccessWhenMatchingToken() {
// name and avatar should be populated from Naver API
ts.createUser("123", "[email protected]", "", "", "invite_token")

tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

u := performAuthorization(ts, "naver", code, "invite_token")

assertAuthorizationSuccess(ts, u, tokenCount, userCount, "[email protected]", "Naver Test", "123", "http://example.com/avatar")
}

func (ts *ExternalTestSuite) TestInviteTokenExternalNaverErrorWhenNoMatchingToken() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

w := performAuthorizationRequest(ts, "naver", "invite_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}

func (ts *ExternalTestSuite) TestInviteTokenExternalNaverErrorWhenWrongToken() {
ts.createUser("123", "[email protected]", "", "", "invite_token")

tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

w := performAuthorizationRequest(ts, "naver", "wrong_token")
ts.Require().Equal(http.StatusNotFound, w.Code)
}

func (ts *ExternalTestSuite) TestInviteTokenExternalNaverErrorWhenEmailDoesntMatch() {
ts.createUser("123", "[email protected]", "", "", "invite_token")

tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

u := performAuthorization(ts, "naver", code, "invite_token")

assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "")
}

func (ts *ExternalTestSuite) TestSignupExternalNaverErrorWhenUserBanned() {
tokenCount, userCount := 0, 0
code := "authcode"
emails := `[{"email":"[email protected]", "primary": true, "verified": true}]`
server := NaverTestSignupSetup(ts, &tokenCount, &userCount, code, emails)
defer server.Close()

u := performAuthorization(ts, "naver", code, "")
assertAuthorizationSuccess(ts, u, tokenCount, userCount, "[email protected]", "Naver Test", "123", "http://example.com/avatar")

user, err := models.FindUserByEmailAndAudience(ts.API.db, "[email protected]", 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, "naver", code, "")
assertAuthorizationFailure(ts, u, "User is banned", "access_denied", "")
}
100 changes: 100 additions & 0 deletions internal/api/provider/naver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package provider

import (
"context"
"strings"

"github.com/supabase/auth/internal/conf"
"golang.org/x/oauth2"
)

const (
defaultNaverAuthBase = "nid.naver.com"
defaultNaverAPIBase = "openapi.naver.com"
IssuerNaver = "https://nid.naver.com"
)

type naverProvider struct {
*oauth2.Config
APIHost string
}

type naverUserResponse struct {
Response struct {
ID string `json:"id"`
Name string `json:"name"`
ProfileImageURL string `json:"profile_image"`
Email string `json:"email"`
} `json:"response"`
}

func (p naverProvider) GetOAuthToken(code string) (*oauth2.Token, error) {
return p.Exchange(context.Background(), code)
}

func (p naverProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) {
var u naverUserResponse

if err := makeRequest(ctx, tok, p.Config, p.APIHost+"/v1/nid/me", &u); err != nil {
return nil, err
}

data := &UserProvidedData{}

if u.Response.Email != "" {
data.Emails = []Email{
{
Email: u.Response.Email,
Verified: true,
Primary: true,
},
}
}

data.Metadata = &Claims{
Issuer: p.APIHost,
Subject: u.Response.ID,

Name: u.Response.Name,
PreferredUsername: u.Response.Name,

// To be deprecated
AvatarURL: u.Response.ProfileImageURL,
FullName: u.Response.Name,
ProviderId: u.Response.ID,
UserNameKey: u.Response.Name,
}
return data, nil
}

func NewNaverProvider(ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) {
if err := ext.ValidateOAuth(); err != nil {
return nil, err
}

authHost := chooseHost(ext.URL, defaultNaverAuthBase)
apiHost := chooseHost(ext.URL, defaultNaverAPIBase)

oauthScopes := []string{
"openid",
}

if scopes != "" {
oauthScopes = append(oauthScopes, strings.Split(scopes, ",")...)
}

return &naverProvider{
Config: &oauth2.Config{
ClientID: ext.ClientID[0],
ClientSecret: ext.Secret,
Endpoint: oauth2.Endpoint{
AuthStyle: oauth2.AuthStyleInParams,
AuthURL: authHost + "/oauth2.0/authorize",
TokenURL: authHost + "/oauth2.0/token",
},
RedirectURL: ext.RedirectURI,
Scopes: oauthScopes,
},
APIHost: apiHost,
}, nil
}
2 changes: 2 additions & 0 deletions internal/api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ProviderSettings struct {
Google bool `json:"google"`
Keycloak bool `json:"keycloak"`
Kakao bool `json:"kakao"`
Naver bool `json:"naver"`
Linkedin bool `json:"linkedin"`
LinkedinOIDC bool `json:"linkedin_oidc"`
Notion bool `json:"notion"`
Expand Down Expand Up @@ -58,6 +59,7 @@ func (a *API) Settings(w http.ResponseWriter, r *http.Request) error {
GitLab: config.External.Gitlab.Enabled,
Google: config.External.Google.Enabled,
Kakao: config.External.Kakao.Enabled,
Naver: config.External.Naver.Enabled,
Keycloak: config.External.Keycloak.Enabled,
Linkedin: config.External.Linkedin.Enabled,
LinkedinOIDC: config.External.LinkedinOIDC.Enabled,
Expand Down
1 change: 1 addition & 0 deletions internal/api/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestSettings_DefaultProviders(t *testing.T) {
require.True(t, p.SlackOIDC)
require.True(t, p.Google)
require.True(t, p.Kakao)
require.True(t, p.Naver)
require.True(t, p.Keycloak)
require.True(t, p.Linkedin)
require.True(t, p.LinkedinOIDC)
Expand Down
1 change: 1 addition & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ type ProviderConfiguration struct {
Gitlab OAuthProviderConfiguration `json:"gitlab"`
Google OAuthProviderConfiguration `json:"google"`
Kakao OAuthProviderConfiguration `json:"kakao"`
Naver OAuthProviderConfiguration `json:"naver"`
Notion OAuthProviderConfiguration `json:"notion"`
Keycloak OAuthProviderConfiguration `json:"keycloak"`
Linkedin OAuthProviderConfiguration `json:"linkedin"`
Expand Down