HomePost

Implementing Naver Login in Supabase (Custom Provider + Edge Function)

2026-04-04

Supabase doesn't support Naver as a provider. Custom OIDC failed due to Naver's non-standard API response. Here's how I solved it with an Edge Function proxy.

TL;DR

Supabase doesn't officially support Naver login. Even with the Custom OIDC Provider feature, Naver's non-standard userinfo response format causes an Error getting user email from external provider error. The fix: place an Edge Function as a userinfo proxy that transforms Naver's response into the standard OIDC format.

Open-sourced here: supabase-naver-oidc-proxy

Background

Translator Pro is a PDF translation service that uses Supabase Auth for authentication. It originally only supported Google OAuth, and I decided to add Naver login for Korean users.

Supabase has built-in support for Kakao, but not Naver. There's an open feature request on GitHub, but it remains unresolved.

Attempt 1: Custom OIDC Provider (Auto-discovery)

I found out that Naver supports OIDC. It even has a discovery endpoint:

text
https://nid.naver.com/.well-known/openid-configuration

I assumed this would work out of the box with Supabase's recently added Custom Provider feature.

Configuration

Supabase Dashboard → Authentication → Custom Providers:

  • Configuration Method: Auto-discovery
  • Issuer URL: https://nid.naver.com
  • Scopes: openid

Result: Failure

text
error: server_error
error_code: unexpected_failure
error_description: Error getting user email from external provider

Why Does It Fail?

Tracking down the root cause took some time. I was able to see the full error message by inspecting the Supabase callback request payload in the browser DevTools Network tab.

The core issue: Naver's userinfo response doesn't conform to the OIDC standard.

What Supabase expects (OIDC standard):

json
{
  "sub": "abc123",
  "email": "user@example.com",
  "email_verified": true,
  "name": "John Doe"
}

What Naver actually returns:

json
{
  "resultcode": "00",
  "message": "success",
  "response": {
    "id": "abc123",
    "email": "user@example.com",
    "name": "John Doe"
  }
}

The user information is nested inside a response object, so Supabase can't find the email field.

Attempt 2: Edge Function as a Userinfo Proxy

Instead of having Supabase Auth call Naver's userinfo API directly, I placed an Edge Function in between to transform the response format.

text
Supabase Auth
  ├→ Authorization URL  →  Naver (direct)
  ├→ Token URL          →  Naver (direct)
  └→ Userinfo URL       →  Edge Function (proxy)
                              └→ Call Naver /v1/nid/me
                              └→ Unwrap the response object
                              └→ Return in standard OIDC format

Edge Function Code

The entire code is under 60 lines:

typescript
const NAVER_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
 
Deno.serve(async (req: Request) => {
  const authorization = req.headers.get("Authorization");
  if (!authorization) {
    return Response.json({ error: "Missing Authorization header" }, { status: 401 });
  }
 
  const naverResponse = await fetch(NAVER_USERINFO_URL, {
    headers: { Authorization: authorization },
  });
 
  if (!naverResponse.ok) {
    return Response.json({ error: "Failed to fetch user info from Naver" }, { status: naverResponse.status });
  }
 
  const data = await naverResponse.json();
  const profile = data.response;
 
  return Response.json({
    sub: profile.id,
    email: profile.email,
    email_verified: true,
    name: profile.name,
    nickname: profile.nickname,
    picture: profile.profile_image,
  });
});

Deploy:

bash
supabase functions deploy naver-userinfo --no-verify-jwt

Why --no-verify-jwt is required: this function receives a Naver access token in the Authorization header, not a Supabase JWT. Without disabling Supabase's JWT verification, the Naver token gets rejected.

Supabase Configuration (Manual)

Switch from Auto-discovery to Manual configuration and point only the userinfo URL to the Edge Function:

FieldValue
Configuration MethodManual configuration
Issuer URLhttps://nid.naver.com
Authorization URLhttps://nid.naver.com/oauth2/authorize
Token URLhttps://nid.naver.com/oauth2/token
Userinfo URLhttps://<project>.supabase.co/functions/v1/naver-userinfo
JWKS URIhttps://nid.naver.com/oauth2/jwks
Scopesprofile

3 Gotchas That Tripped Me Up

Getting this to actually work involved a few more hidden pitfalls.

1. Scopes Must Be profile Only

I initially included openid in the scopes, which caused Supabase to try extracting user information from the id_token instead — and it never called the userinfo endpoint at all. The Edge Function Invocations dashboard showed zero calls.

Removing openid and keeping only profile forces Supabase to use the userinfo endpoint instead of the id_token.

2. Email Must Be Set to "Required" in the Naver Developer Console

In the Naver Developer Console → API Permission Settings, the email field must be set to "Required consent." Otherwise, the Naver API won't return the email. Even if the Edge Function works correctly, an empty email field will trigger the same Supabase error.

3. --no-verify-jwt Is Essential

If you forget this flag when deploying the Edge Function, the Supabase Edge Function runtime will try to validate the Naver access token as a Supabase JWT, returning a 401.

Client Code

The code to trigger login from a button is straightforward:

typescript
const { error } = await supabase.auth.signInWithOAuth({
  // Type assertion needed because the Supabase SDK doesn't support custom providers in its types yet
  provider: "custom:naver" as "google",
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
  },
});

Per Naver's BI guidelines, the button should use a green background (#03A94D) with a white N logo.

Open Source

I open-sourced this for other Korean developers facing the same problem. It includes the Edge Function code and a detailed setup guide.

GitHub: supabase-naver-oidc-proxy

Wrapping Up

Supabase's Custom Provider works well for providers that follow the OIDC standard, but for non-standard providers like Naver, an Edge Function proxy is needed. It's a 60-line function that solves the problem, but figuring out where it was failing took the longest.

Since Kakao has built-in Supabase support, a Korean service can cover most users with a Kakao + Naver (using this approach) combination.