socialiteproviders/kakao
Kakao provider for Laravel Socialite adds OAuth login with Kakao to your application. Install via Composer, configure Kakao credentials, and use Socialite's familiar redirect and callback flow to authenticate users—no custom OAuth code required.
Install both halves. The provider package is a thin driver; Socialite itself is a separate dependency.
composer require laravel/socialite socialiteproviders/kakao
socialiteproviders/kakao pulls in socialiteproviders/manager, which is what teaches Socialite to resolve third‑party drivers by name. If you're on a Laravel version where Socialite isn't in the skeleton by default, install it explicitly — a missing laravel/socialite shows up as a confusing "Driver [kakao] is not supported" later.
Add credentials to config/services.php:
'kakao' => [
'client_id' => env('KAKAO_CLIENT_ID'),
'client_secret' => env('KAKAO_CLIENT_SECRET'), // optional — see Gotchas
'redirect' => env('KAKAO_REDIRECT_URI'),
],
Do the Kakao console work before writing code. At developers.kakao.com → your app → Kakao Login:
KAKAO_REDIRECT_URI.profile_nickname, profile_image, account_email, etc.This console step — not the PHP — is where the majority of first‑run failures originate. Skipping it means your redirect works and your callback throws.
Register the provider. With socialiteproviders/manager v4 the driver is resolved by convention, so Socialite::driver('kakao') frequently just works. If it doesn't, register the listener explicitly — in app/Providers/EventServiceProvider.php on older Laravel, or AppServiceProvider::boot() on Laravel 11+:
use SocialiteProviders\Manager\SocialiteWasCalled;
Event::listen(function (SocialiteWasCalled $event) {
$event->extendWith('kakao', \SocialiteProviders\Kakao\KakaoExtendSocialite::class);
});
Your first use case — a redirect/callback pair:
Route::get('/auth/kakao/redirect', fn () => Socialite::driver('kakao')
->setScopes(['profile_nickname', 'profile_image'])
->redirect());
Route::get('/auth/kakao/callback', function () {
$kakao = Socialite::driver('kakao')->user();
$user = User::updateOrCreate(
['provider' => 'kakao', 'provider_id' => $kakao->getId()],
[
'name' => $kakao->getNickname(),
'email' => $kakao->getEmail(), // may be null
'avatar' => $kakao->getAvatar(),
],
);
Auth::login($user);
return redirect()->intended('/dashboard');
});
Mapped accessors are getId(), getNickname(), getName(), getEmail(), getAvatar(). Anything Kakao returns that isn't mapped is still available on $kakao->user and $kakao->accessTokenResponseBody.
Key on provider_id, never on email. Kakao's numeric user id is stable; email is conditional (see Gotchas). Store the pair (provider, provider_id) so Naver, Apple, Google and Kakao can share one users table. Type provider_id as string or bigInteger — Kakao ids are large integers serialised as strings and will bite you if you declare int.
Centralise scopes in config, not in routes. setScopes([...]) must be a subset of both the consent items registered in the console and the scopes enabled on the app. Put the list in config/services.php or a dedicated constant so the redirect route and the Kakao console can't silently drift:
$scopes = config('services.kakao.scopes'); // ['profile_nickname', 'profile_image']
return Socialite::driver('kakao')->setScopes($scopes)->redirect();
Wrap the flow in a service, not a controller. The controller should orchestrate HTTP; the provisioning logic belongs in a dedicated class you can test without Socialite:
final class KakaoLoginService
{
public function handle(\Laravel\Socialite\Two\User $kakao): User
{
return User::updateOrCreate(
['provider' => 'kakao', 'provider_id' => $kakao->getId()],
['name' => $kakao->getNickname(), 'avatar' => $kakao->getAvatar()],
);
}
}
This is the piece worth unit-testing — it holds your identity rules, and it's the part that breaks when you refactor.
Stateless / token‑based flows for SPAs and mobile. When the client already holds a Kakao access token, skip the redirect entirely:
Route::middleware('auth:sanctum')->post('/auth/kakao/token', function (Request $request) {
$kakao = Socialite::driver('kakao')
->stateless()
->userFromToken($request->string('kakao_access_token'));
$user = app(KakaoLoginService::class)->handle($kakao);
return ['token' => $user->createToken('kakao')->plainTextToken];
});
Return your own API token. Do not attempt to start a session from a stateless call.
Persist tokens when you'll act on the user's behalf. $kakao->token, $kakao->refreshToken, and $kakao->expiresIn are populated on the callback. Store the refresh token with an encrypted cast and record an expiry — the package will not refresh it for you:
$user->forceFill([
'kakao_access_token' => encrypt($kakao->token),
'kakao_refresh_token' => encrypt($kakao->refreshToken),
'kakao_expires_at' => now()->addSeconds($kakao->expiresIn),
])->save();
Reach past the package deliberately. This driver covers login and profile fetch only. Unlink (POST https://kapi.kakao.com/v1/user/unlink) and KakaoTalk "send to me" (/v2/api/talk/memo/default/send, needs the talk_message scope) are plain HTTP calls you make yourself:
Http::withToken($user->kakao_access_token)->post('https://kapi.kakao.com/v1/user/unlink');
That's normal — don't wait for provider methods that will never exist.
Custom user mapping when you need more fields. For kakao_account.gender, birthday, phone_number, or properties.*, don't scatter $user->user['kakao_account'][...] through controllers. Subclass the provider and override mapUserToObject(), registering it in a service provider:
Socialite::extend('kakao', fn ($app) => new MyKakaoProvider(
$app['request'], $app['config']['services.kakao']
));
One place to change when Kakao reshapes its payload.
Testing strategy. Socialite has no fake() helper, so pick one of two approaches:
Socialite::shouldReceive('driver')->with('kakao')->andReturn($mockProvider);
$mockProvider->shouldReceive('user')->andReturn($fakeKakaoUser);
Http::fake() the token and user endpoints and run the real provider from a feature test, which also exercises the driver's URL construction.Either way, build a small helper that returns a Laravel\Socialite\Two\User populated with representative Kakao fields, so tests don't drift from reality.
Multi‑tenant / multi‑app. If you run separate Kakao apps per brand, key services.kakao off the tenant and register separate driver names (kakao_shop, kakao_admin) with one config entry each. The driver name is global to Socialite; overriding config at runtime is messier than aliasing.
Email is conditional and nullable. account_email requires the consent item to be approved in the console (and, for some app categories, a Kakao review). Until then getEmail() returns null. Make the column nullable, and never treat email as the identity key. is_email_verified can also be false even when an email is present.
Client secret must match on both sides. Setting KAKAO_CLIENT_SECRET without toggling Client Secret in the Kakao console fails token exchange with KOE010. Leaving it empty when the console demands it fails just as hard. Match exactly.
Redirect URI is byte‑exact. Trailing slash, http vs https, localhost vs 127.0.0.1 — any difference produces redirect_uri_mismatch at the authorize step, which reads like an application bug. Kakao permits http only for localhost; use ngrok or Expose for anything else in development.
Scope errors surface late. Requesting a scope that isn't a registered consent item throws on the callback, not the redirect. Log the exception message — Kakao's KOE codes name the offending scope directly, which saves you from guessing.
InvalidStateException means the session was lost between legs. Domain mismatch, SESSION_DOMAIN misconfiguration, or calling stateless() on one leg and not the other. It is not a package bug — check session config before filing an issue.
Nicknames are mutable and non‑unique. Users can change them freely and duplicates are allowed. Never key, search, or deduplicate on nickname.
Token lifetimes. Kakao access tokens last roughly 6 hours; refresh tokens roughly 2 months and rotate on use. Refresh proactively via POST https://kauth.kakao.com/oauth/token with grant_type=refresh_token before calling any Kakao API on the user's behalf — don't wait for a 401.
getId() returns a string. Don't cast it to int in a model accessor. It works until it doesn't, on 32‑bit systems or when comparing against a JSON‑decoded integer.
Debug in a fixed order. It covers roughly 95% of reports:
Socialite::driver('kakao') resolve at all? → missing listener or Manager.Version 4.3.0 carries no documented breaking changes — it's a routine minor bump from 4.2.2. Update with composer update socialiteproviders/kakao, then re-run one real login against a sandbox Kakao app. These releases track Socialite and Manager majors, so a transitive bump can move the SocialiteWasCalled API under you without any change in this package's own diff.
Assign an internal owner. This is community‑maintained with no SLA. Someone on the team should own OAuth configuration, dependency updates, and Kakao developer‑console administration — otherwise the next API change or console deprecation is discovered by users, not by you.
How can I help you explore Laravel packages today?