Laravel Passport
번역일: 2026년 7월 2일
Laravel Passport
- 소개
- 설치
- 설정
- 인가 코드 그랜트
- PKCE를 사용한 인가 코드 그랜트
- 디바이스 인가 그랜트
- 패스워드 그랜트
- 임플리싯 그랜트
- 클라이언트 자격증명 그랜트
- 개인 액세스 토큰
- 라우트 보호
- 토큰 스코프
- SPA 인증
- 이벤트
- 테스트
소개
Laravel Passport는 Laravel 애플리케이션에 완전한 OAuth2 서버 기능을 제공합니다. Passport는 League OAuth2 server 패키지를 기반으로 하며, Andy Millington과 Simon Hamp가 관리하고 있습니다.
WARNING
이 문서는 독자가 OAuth2에 이미 익숙하다는 전제 하에 작성되었습니다. OAuth2를 처음 접하신다면 먼저 OAuth2의 기본 용어와 전반적인 개념을 파악한 뒤 진행하시기를 권장합니다.
Passport vs Sanctum
본격적으로 시작하기 전에, 프로젝트에 Laravel Passport와 Laravel Sanctum 중 어느 쪽이 더 적합한지 먼저 판단하는 것이 좋습니다.
애플리케이션이 반드시 OAuth2를 지원해야 한다면 Laravel Passport를 선택하세요. OAuth2는 외부 서비스나 서드파티 애플리케이션이 여러분의 API에 접근할 수 있도록 액세스 토큰을 발급하는 표준 프로토콜입니다. 사용자가 "OO 계정으로 로그인" 버튼을 통해 직접 자격증명을 노출하지 않고도 서드파티에 제한된 권한을 부여할 수 있습니다.
반면, 자체 퍼스트파티 SPA나 모바일 앱을 위한 API 인증이 목적이라면 Laravel Sanctum을 권장합니다. Sanctum은 OAuth2를 구현하지 않지만, 훨씬 단순한 개발 경험을 제공합니다. 실제로 많은 프로젝트에서 OAuth2의 복잡성은 불필요하며, Sanctum만으로도 충분합니다.
요약하자면, Passport는 외부 서드파티와의 OAuth2 연동이 필요한 경우에, Sanctum은 자체 서비스 내 SPA나 모바일 앱 인증에 적합합니다.
설치
Passport는 install:api Artisan 명령어로 시작할 수 있습니다.
php artisan install:api --passport이 명령어를 실행하면 OAuth2 클라이언트와 액세스 토큰을 저장하기 위한 데이터베이스 마이그레이션이 게시되고 실행됩니다. 또한 보안 토큰 생성에 필요한 암호화 키도 함께 생성됩니다.
추가로, 이 명령어는 config/auth.php 파일에서 api 인증 가드의 driver를 passport로 설정해야 하는지 물어봅니다. 이를 설정하면 API 요청을 인증할 때 Passport의 TokenGuard가 사용됩니다.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],다음으로, Passport로 보호할 모델에 Laravel\Passport\HasApiTokens 트레이트를 추가하세요. 일반적으로 App\Models\User 모델에 추가합니다. 이 트레이트는 인증된 사용자의 토큰과 스코프를 확인하는 헬퍼 메서드를 모델에 제공합니다.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}마지막으로, 애플리케이션의 bootstrap/app.php 파일에서 withRouting 메서드의 api 라우트 파일이 실제로 사용될 routes/api.php 파일을 정의하고 있는지 확인하세요. Passport의 OAuth2 라우트는 이 파일을 통해 등록됩니다.
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
// ...
)Passport의 라우트는 클라이언트 관리와 토큰 발급/취소 등의 OAuth2 흐름에 필요한 엔드포인트를 담당합니다. 라우트 목록은 vendor/laravel/passport/routes/api.php 파일에서 확인할 수 있습니다.
Passport 배포
처음으로 서버에 Passport를 배포할 때는 passport:keys 명령어를 실행해야 합니다. 이 명령어는 액세스 토큰 생성에 필요한 암호화 키를 생성합니다. 생성된 키는 일반적으로 버전 관리 시스템(Git 등)에 포함하지 않습니다.
php artisan passport:keys필요한 경우 Passport 키를 로드할 경로를 직접 지정할 수 있습니다. Passport::loadKeysFrom 메서드를 사용하면 됩니다. 보통 애플리케이션의 AppServiceProvider에서 호출합니다.
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
}환경 변수에서 키 로드하기
서버 환경에 따라서는 키 파일을 직접 관리하기 어려울 수 있습니다. 예를 들어, AWS나 Kubernetes 같은 환경에서는 파일 시스템 대신 환경 변수나 시크릿 스토어를 활용하는 것이 일반적입니다. 이런 경우 .env 파일에 키 값을 직접 지정할 수 있습니다.
PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<프라이빗 키 내용>
-----END RSA PRIVATE KEY-----"
PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<퍼블릭 키 내용>
-----END PUBLIC KEY-----"Passport 업그레이드
Passport를 새로운 메이저 버전으로 업그레이드할 때는 반드시 업그레이드 가이드를 꼼꼼히 확인하세요.
설정
토큰 유효 기간
Passport는 기본적으로 액세스 토큰의 유효 기간을 1년으로 설정합니다. 유효 기간을 변경하려면 tokensExpireIn, refreshTokensExpireIn, personalAccessTokensExpireIn 메서드를 사용하세요. 이 메서드들은 일반적으로 AppServiceProvider의 boot 메서드에서 호출합니다.
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}WARNING
Passport 데이터베이스 테이블의 expires_at 컬럼은 읽기 전용으로, 표시 목적으로만 사용됩니다. 토큰을 발급할 때 Passport는 만료 정보를 서명된 암호화 토큰 내부에 저장합니다. 토큰을 무효화해야 한다면 취소 처리를 해야 합니다.
기본 모델 재정의
Passport 내부적으로 사용하는 모델을 직접 확장하여 커스터마이징할 수 있습니다. 커스텀 모델을 만들고 해당 Passport 모델을 상속하면 됩니다.
use Laravel\Passport\Client as PassportClient;
class Client extends PassportClient
{
// 커스터마이징 내용 추가
}모델을 정의한 뒤, Laravel\Passport\Passport 클래스를 통해 Passport에 커스텀 모델을 등록합니다. 마찬가지로 AppServiceProvider의 boot 메서드에서 호출하면 됩니다.
use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\DeviceCode;
use App\Models\Passport\PersonalAccessClient;
use App\Models\Passport\RefreshToken;
use App\Models\Passport\Token;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::useTokenModel(Token::class);
Passport::useRefreshTokenModel(RefreshToken::class);
Passport::useAuthCodeModel(AuthCode::class);
Passport::useClientModel(Client::class);
Passport::useDeviceCodeModel(DeviceCode::class);
Passport::usePersonalAccessClientModel(PersonalAccessClient::class);
}라우트 재정의
Passport가 기본으로 등록하는 라우트를 직접 제어하고 싶을 때가 있습니다. 예를 들어, 특정 라우트에 추가 미들웨어를 적용하거나 일부 라우트를 비활성화하고 싶을 수 있습니다. 이런 경우 AppServiceProvider의 boot 메서드에서 Passport::ignoreRoutes()를 호출하여 Passport의 자동 라우트 등록을 막은 뒤, 직접 라우트를 정의하면 됩니다.
use Laravel\Passport\Passport;
Passport::ignoreRoutes();그런 다음, Passport의 라우트 파일에서 필요한 라우트를 복사하여 애플리케이션의 routes/api.php 파일에 붙여넣고 원하는 대로 수정하면 됩니다.
use Laravel\Passport\Http\Controllers\AccessTokenController;
use Laravel\Passport\Http\Controllers\AuthorizationController;
use Laravel\Passport\Http\Controllers\AuthorizedAccessTokenController;
use Laravel\Passport\Http\Controllers\ClientController;
use Laravel\Passport\Http\Controllers\DeviceAuthorizationController;
use Laravel\Passport\Http\Controllers\PersonalAccessTokenController;
use Laravel\Passport\Http\Controllers\ScopeController;
use Laravel\Passport\Http\Controllers\TransientTokenController;
Route::group([
'as' => 'passport.',
'middleware' => ['web', 'throttle'],
'prefix' => config('passport.path', 'oauth'),
], function () {
Route::post('/token', [AccessTokenController::class, 'issueToken'])->name('token');
// 나머지 라우트를 필요에 따라 추가...
});Laravel Passport
소개
Laravel Passport는 Laravel 애플리케이션에 완전한 OAuth2 서버 기능을 빠르게 추가할 수 있도록 도와주는 공식 패키지입니다. Passport는 Andy Millington과 Simon Hamp가 관리하는 League OAuth2 server 위에 구축되어 있습니다.
NOTE
이 문서는 독자가 OAuth2의 기본 개념을 이미 알고 있다고 가정합니다. OAuth2가 처음이라면, 본 문서를 읽기 전에 공식 용어 설명과 OAuth2의 주요 개념을 먼저 파악해 두는 것을 권장합니다.
Passport를 쓸까요, Sanctum을 쓸까요?
시작하기 전에, 여러분의 애플리케이션에 Laravel Passport와 Laravel Sanctum 중 어떤 것이 더 적합한지 먼저 판단해 보세요.
OAuth2 프로토콜을 반드시 지원해야 한다면 Laravel Passport를 사용하세요.
반면, SPA(싱글 페이지 애플리케이션), 모바일 앱, 또는 단순한 API 토큰 발급이 목적이라면 Laravel Sanctum을 사용하는 것이 좋습니다. Sanctum은 OAuth2를 지원하지 않지만, API 인증을 훨씬 간단하게 구현할 수 있습니다.
NOTE
실제로 많은 프로젝트에서 OAuth2의 복잡한 권한 위임 흐름이 필요하지 않은 경우가 많습니다. 외부 서드파티 앱에 API를 개방하거나, 소셜 로그인 제공자처럼 동작해야 하는 경우가 아니라면 Sanctum으로 충분한 경우가 대부분입니다.
설치
install:api Artisan 명령어로 Laravel Passport를 설치할 수 있습니다:
php artisan install:api --passport이 명령어는 OAuth2 클라이언트와 액세스 토큰을 저장하는 데 필요한 데이터베이스 마이그레이션을 게시하고 실행합니다. 또한 안전한 액세스 토큰을 생성하기 위한 암호화 키도 함께 생성됩니다.
install:api 명령어 실행이 끝나면, App\Models\User 모델에 Laravel\Passport\HasApiTokens 트레이트와 Laravel\Passport\Contracts\OAuthenticatable 인터페이스를 추가하세요. 이 트레이트는 인증된 사용자의 토큰과 스코프를 확인할 수 있는 몇 가지 헬퍼 메서드를 모델에 제공합니다:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable implements OAuthenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}마지막으로, config/auth.php 설정 파일에서 api 인증 가드를 정의하고 driver 옵션을 passport로 지정합니다. 이렇게 하면 API 요청 인증 시 Passport의 TokenGuard가 사용됩니다:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],Passport 배포
처음으로 서버에 Passport를 배포할 때는 passport:keys 명령어를 실행해야 합니다. 이 명령어는 액세스 토큰 생성에 필요한 암호화 키를 만듭니다. 생성된 키는 일반적으로 소스 코드 저장소에 포함하지 않습니다:
php artisan passport:keysNOTE
암호화 키 파일을 Git 저장소에 커밋하지 않도록 주의하세요. .gitignore에 키 파일 경로를 추가하는 것을 권장합니다.
필요에 따라 Passport가 키를 불러올 경로를 직접 지정할 수도 있습니다. Passport::loadKeysFrom 메서드를 사용하며, 일반적으로 App\Providers\AppServiceProvider의 boot 메서드에서 호출합니다:
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
}환경 변수에서 키 불러오기
또는 vendor:publish Artisan 명령어로 Passport 설정 파일을 게시한 뒤:
php artisan vendor:publish --tag=passport-config암호화 키를 환경 변수로 정의해 불러올 수도 있습니다:
PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"
PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"이 방식은 배포 환경마다 키 파일을 직접 관리하기 어려울 때 유용합니다. CI/CD 파이프라인이나 컨테이너 환경에서 환경 변수로 키를 주입하면 파일 경로 문제 없이 간편하게 운영할 수 있습니다.
Passport 업그레이드
Passport의 새로운 메이저 버전으로 업그레이드할 때는 업그레이드 가이드를 반드시 꼼꼼히 확인하세요. 메이저 버전 간에는 데이터베이스 스키마나 설정 구조가 변경될 수 있습니다.
설정
토큰 유효 기간
Passport는 기본적으로 만료 기간이 1년인 액세스 토큰을 발급합니다. 유효 기간을 더 길거나 짧게 조정하려면 tokensExpireIn, refreshTokensExpireIn, personalAccessTokensExpireIn 메서드를 사용하세요. 이 메서드들은 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 호출해야 합니다.
use Carbon\CarbonInterval;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::tokensExpireIn(CarbonInterval::days(15));
Passport::refreshTokensExpireIn(CarbonInterval::days(30));
Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));
}WARNING
Passport 데이터베이스 테이블의 expires_at 컬럼은 읽기 전용으로, 표시 목적으로만 사용됩니다. 실제 만료 정보는 서명·암호화된 토큰 내부에 저장됩니다. 토큰을 무효화해야 할 경우에는 토큰을 폐기하세요.
기본 모델 오버라이딩
Passport가 내부적으로 사용하는 모델을 직접 확장할 수 있습니다. 커스텀 모델을 정의할 때는 해당 Passport 모델을 상속하세요.
use Laravel\Passport\Client as PassportClient;
class Client extends PassportClient
{
// ...
}모델을 정의한 후에는 App\Providers\AppServiceProvider의 boot 메서드에서 Passport가 커스텀 모델을 사용하도록 등록합니다.
use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\DeviceCode;
use App\Models\Passport\RefreshToken;
use App\Models\Passport\Token;
use Laravel\Passport\Passport;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::useTokenModel(Token::class);
Passport::useRefreshTokenModel(RefreshToken::class);
Passport::useAuthCodeModel(AuthCode::class);
Passport::useClientModel(Client::class);
Passport::useDeviceCodeModel(DeviceCode::class);
}라우트 오버라이딩
Passport가 등록하는 기본 라우트를 직접 커스터마이징하고 싶을 때가 있습니다. 이 경우 먼저 AppServiceProvider의 register 메서드에서 Passport::ignoreRoutes를 호출해 Passport의 기본 라우트 등록을 비활성화합니다.
use Laravel\Passport\Passport;
/**
* 애플리케이션 서비스를 등록합니다.
*/
public function register(): void
{
Passport::ignoreRoutes();
}그런 다음 Passport의 라우트 파일에서 필요한 라우트를 복사해 애플리케이션의 routes/web.php에 붙여넣고 원하는 대로 수정하세요.
Route::group([
'as' => 'passport.',
'prefix' => config('passport.path', 'oauth'),
'namespace' => '\Laravel\Passport\Http\Controllers',
], function () {
// Passport 라우트...
});인증 코드 그랜트 (Authorization Code Grant)
OAuth2에서 가장 널리 사용되는 방식이 바로 인증 코드 그랜트입니다. 클라이언트 애플리케이션이 사용자를 여러분의 서버로 리다이렉트하면, 사용자는 해당 클라이언트에 액세스 토큰을 발급할지 승인하거나 거부하게 됩니다.
시작하기 전에, Passport에게 "인증(authorization)" 뷰를 어떻게 반환할지 알려줘야 합니다.
인증 뷰의 렌더링 로직은 Laravel\Passport\Passport 클래스의 메서드를 통해 자유롭게 커스터마이징할 수 있습니다. 보통 App\Providers\AppServiceProvider의 boot 메서드에서 설정합니다:
use Inertia\Inertia;
use Laravel\Passport\Passport;
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
// 뷰 이름으로 지정하는 방법...
Passport::authorizationView('auth.oauth.authorize');
// 클로저로 지정하는 방법...
Passport::authorizationView(
fn ($parameters) => Inertia::render('Auth/OAuth/Authorize', [
'request' => $parameters['request'],
'authToken' => $parameters['authToken'],
'client' => $parameters['client'],
'user' => $parameters['user'],
'scopes' => $parameters['scopes'],
])
);
}Passport는 이 뷰를 반환하는 /oauth/authorize 라우트를 자동으로 등록합니다. auth.oauth.authorize 템플릿에는 다음 두 가지 폼이 포함되어야 합니다.
- 승인 시:
passport.authorizations.approve라우트로 POST 요청 - 거부 시:
passport.authorizations.deny라우트로 DELETE 요청
두 라우트 모두 state, client_id, auth_token 필드를 필요로 합니다.
클라이언트 관리
여러분의 API를 사용하려는 개발자는 자신의 애플리케이션을 "클라이언트"로 등록해야 합니다. 등록 시에는 애플리케이션 이름과, 사용자가 인증을 승인한 후 리다이렉트될 URI를 제공합니다.
자사(First-Party) 클라이언트
클라이언트를 가장 간단하게 만드는 방법은 passport:client Artisan 명령어를 사용하는 것입니다. 자사 클라이언트를 만들거나 OAuth2 기능을 테스트할 때 유용합니다. 명령어를 실행하면 Passport가 필요한 정보를 묻고 클라이언트 ID와 시크릿을 발급합니다:
php artisan passport:client하나의 클라이언트에 여러 리다이렉트 URI를 허용하려면, URI 입력 프롬프트에서 쉼표로 구분하여 입력합니다. 쉼표가 포함된 URI는 URI 인코딩해야 합니다:
https://third-party-app.com/callback,https://example.com/oauth/redirect서드파티(Third-Party) 클라이언트
일반 사용자는 passport:client 명령어를 직접 실행할 수 없습니다. 따라서 Laravel\Passport\ClientRepository 클래스의 createAuthorizationCodeGrantClient 메서드를 사용해 특정 사용자에게 속하는 클라이언트를 등록할 수 있습니다:
use App\Models\User;
use Laravel\Passport\ClientRepository;
$user = User::find($userId);
// 해당 사용자에게 속하는 OAuth 앱 클라이언트 생성...
$client = app(ClientRepository::class)->createAuthorizationCodeGrantClient(
user: $user,
name: 'Example App',
redirectUris: ['https://third-party-app.com/callback'],
confidential: false,
enableDeviceFlow: true
);
// 해당 사용자의 모든 OAuth 앱 클라이언트 조회...
$clients = $user->oauthApps()->get();createAuthorizationCodeGrantClient 메서드는 Laravel\Passport\Client 인스턴스를 반환합니다. $client->id를 클라이언트 ID로, $client->plainSecret을 클라이언트 시크릿으로 사용자에게 표시하면 됩니다.
토큰 요청
인증을 위한 리다이렉트
클라이언트가 생성되면, 개발자는 클라이언트 ID와 시크릿을 사용해 인증 코드와 액세스 토큰을 요청할 수 있습니다. 먼저 소비 애플리케이션이 여러분의 /oauth/authorize 라우트로 사용자를 리다이렉트해야 합니다:
use Illuminate\Http\Request;
use Illuminate\Support\Str;
Route::get('/redirect', function (Request $request) {
$request->session()->put('state', $state = Str::random(40));
$query = http_build_query([
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'code',
'scope' => 'user:read orders:create',
'state' => $state,
// 'prompt' => '', // "none", "consent", "login" 중 하나
]);
return redirect('https://passport-app.test/oauth/authorize?'.$query);
});prompt 파라미터로 Passport 애플리케이션의 인증 동작 방식을 지정할 수 있습니다.
prompt 값 | 동작 |
|---|---|
none | 사용자가 이미 로그인되어 있지 않으면 항상 인증 오류를 반환 |
consent | 이전에 모든 스코프를 승인했더라도 항상 승인 화면을 표시 |
login | 기존 세션이 있더라도 항상 재로그인 요청 |
| (생략) | 해당 스코프를 이전에 승인하지 않은 경우에만 승인 화면 표시 |
NOTE
/oauth/authorize 라우트는 Passport가 자동으로 등록합니다. 직접 정의할 필요가 없습니다.
요청 승인
인증 요청이 들어오면 Passport는 prompt 파라미터 값에 따라 자동으로 처리하거나, 사용자에게 승인/거부 화면을 표시합니다. 사용자가 승인하면 클라이언트 생성 시 지정했던 redirect_uri로 리다이렉트됩니다. redirect_uri는 클라이언트 등록 시 설정한 주소와 반드시 일치해야 합니다.
자사 클라이언트처럼 승인 화면을 건너뛰고 싶은 경우, Client 모델을 확장하고 skipsAuthorization 메서드를 정의하면 됩니다. 이 메서드가 true를 반환하면, 소비 애플리케이션이 prompt 파라미터를 명시적으로 설정하지 않은 한 승인 화면 없이 즉시 redirect_uri로 이동합니다:
<?php
namespace App\Models\Passport;
use Illuminate\Contracts\Auth\Authenticatable;
use Laravel\Passport\Client as BaseClient;
class Client extends BaseClient
{
/**
* 클라이언트가 승인 화면을 건너뛸지 결정합니다.
*
* @param \Laravel\Passport\Scope[] $scopes
*/
public function skipsAuthorization(Authenticatable $user, array $scopes): bool
{
return $this->firstParty();
}
}인증 코드를 액세스 토큰으로 교환
사용자가 요청을 승인하면 소비 애플리케이션의 redirect_uri로 리다이렉트됩니다. 이때 소비 애플리케이션은 먼저 state 파라미터가 리다이렉트 전에 저장했던 값과 일치하는지 확인해야 합니다. 값이 일치하면 여러분의 애플리케이션으로 POST 요청을 보내 액세스 토큰을 발급받습니다:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
Route::get('/callback', function (Request $request) {
$state = $request->session()->pull('state');
throw_unless(
strlen($state) > 0 && $state === $request->state,
InvalidArgumentException::class,
'유효하지 않은 state 값입니다.'
);
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret',
'redirect_uri' => 'https://third-party-app.com/callback',
'code' => $request->code,
]);
return $response->json();
});/oauth/token 라우트는 access_token, refresh_token, expires_in 속성을 포함한 JSON 응답을 반환합니다. expires_in은 액세스 토큰이 만료되기까지 남은 초(second) 수입니다.
NOTE
/oauth/authorize 라우트와 마찬가지로, /oauth/token 라우트도 Passport가 자동으로 등록합니다. 직접 정의할 필요가 없습니다.
토큰 관리
Laravel\Passport\HasApiTokens 트레이트의 tokens 메서드를 사용하면 사용자가 승인한 토큰 목록을 조회할 수 있습니다. 예를 들어, 사용자가 연결된 서드파티 애플리케이션을 대시보드에서 확인하고 관리할 수 있도록 구현할 때 활용할 수 있습니다:
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;
$user = User::find($userId);
// 사용자의 유효한 토큰 전체 조회...
$tokens = $user->tokens()
->where('revoked', false)
->where('expires_at', '>', Date::now())
->get();
// 서드파티 OAuth 앱과의 연결 목록 조회...
$connections = $tokens->load('client')
->reject(fn (Token $token) => $token->client->firstParty())
->groupBy('client_id')
->map(fn (Collection $tokens) => [
'client' => $tokens->first()->client,
'scopes' => $tokens->pluck('scopes')->flatten()->unique()->values()->all(),
'tokens_count' => $tokens->count(),
])
->values();토큰 갱신
애플리케이션이 수명이 짧은 액세스 토큰을 발급하는 경우, 사용자는 최초 토큰 발급 시 함께 받은 리프레시 토큰을 사용해 액세스 토큰을 갱신해야 합니다:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'refresh_token',
'refresh_token' => 'the-refresh-token',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret', // 기밀(confidential) 클라이언트만 필요...
'scope' => 'user:read orders:create',
]);
return $response->json();/oauth/token 라우트는 access_token, refresh_token, expires_in 속성을 포함한 JSON 응답을 반환합니다. expires_in은 새로 발급된 액세스 토큰이 만료되기까지 남은 초 수입니다.
토큰 폐기
Laravel\Passport\Token 모델의 revoke 메서드로 액세스 토큰을, Laravel\Passport\RefreshToken 모델의 revoke 메서드로 리프레시 토큰을 폐기할 수 있습니다:
use Laravel\Passport\Passport;
use Laravel\Passport\Token;
$token = Passport::token()->find($tokenId);
// 액세스 토큰 폐기...
$token->revoke();
// 리프레시 토큰 폐기...
$token->refreshToken?->revoke();
// 사용자의 모든 토큰 폐기...
User::find($userId)->tokens()->each(function (Token $token) {
$token->revoke();
$token->refreshToken?->revoke();
});토큰 정리
폐기되었거나 만료된 토큰은 데이터베이스에서 주기적으로 삭제하는 것이 좋습니다. Passport에 내장된 passport:purge Artisan 명령어를 사용하면 됩니다:
<h1 id="creating-a-device-authorization-grant-client">폐기 및 만료된 토큰, 인증 코드, 디바이스 코드 모두 삭제...</h1>
php artisan passport:purge
<h1 id="requesting-device-authorization-grant-tokens">만료된 지 6시간 이상 지난 토큰만 삭제...</h1>
php artisan passport:purge --hours=6
<h1 id="device-code">폐기된 토큰, 인증 코드, 디바이스 코드만 삭제...</h1>
php artisan passport:purge --revoked
<h1 id="user-code">만료된 토큰, 인증 코드, 디바이스 코드만 삭제...</h1>
php artisan passport:purge --expiredroutes/console.php 파일에 스케줄 작업을 등록해 자동으로 정리되도록 설정할 수도 있습니다:
use Illuminate\Support\Facades\Schedule;
Schedule::command('passport:purge')->hourly();PKCE를 활용한 인가 코드 그랜트
"Proof Key for Code Exchange"(PKCE)를 적용한 인가 코드 그랜트는 싱글 페이지 애플리케이션(SPA)이나 모바일 앱에서 API에 안전하게 접근하기 위한 방법입니다. 클라이언트 시크릿을 안전하게 보관하기 어렵거나, 인가 코드가 중간에 탈취될 위험이 있는 환경에서 특히 유용합니다. PKCE는 클라이언트 시크릿 대신 "코드 검증자(code verifier)"와 "코드 챌린지(code challenge)"의 쌍을 활용해 액세스 토큰을 발급합니다.
클라이언트 생성
PKCE 방식으로 토큰을 발급하려면 먼저 PKCE가 활성화된 클라이언트를 생성해야 합니다. passport:client Artisan 명령에 --public 옵션을 붙여 실행하세요.
php artisan passport:client --public토큰 요청
코드 검증자와 코드 챌린지
PKCE 방식은 클라이언트 시크릿 없이 동작하므로, 토큰을 요청하기 전에 코드 검증자(code verifier)와 코드 챌린지(code challenge)를 직접 생성해야 합니다.
코드 검증자는 RFC 7636 명세에 따라 영문자, 숫자, 그리고 "-", ".", "_", "~" 문자로 구성된 43~128자 길이의 랜덤 문자열이어야 합니다.
코드 챌린지는 코드 검증자를 SHA-256으로 해시한 뒤, URL 및 파일명에 안전한 문자로 Base64 인코딩한 값입니다. 끝의 '=' 패딩 문자는 제거하고, 줄바꿈이나 공백 등 추가 문자가 포함되어서는 안 됩니다.
$encoded = base64_encode(hash('sha256', $codeVerifier, true));
$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');인가 페이지로 리다이렉트
클라이언트를 생성했다면, 클라이언트 ID와 생성한 코드 검증자·코드 챌린지를 이용해 인가 코드를 요청할 수 있습니다. 먼저 클라이언트 애플리케이션에서 Passport 서버의 /oauth/authorize 라우트로 사용자를 리다이렉트합니다.
use Illuminate\Http\Request;
use Illuminate\Support\Str;
Route::get('/redirect', function (Request $request) {
$request->session()->put('state', $state = Str::random(40));
$request->session()->put(
'code_verifier', $codeVerifier = Str::random(128)
);
$codeChallenge = strtr(rtrim(
base64_encode(hash('sha256', $codeVerifier, true))
, '='), '+/', '-_');
$query = http_build_query([
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'code',
'scope' => 'user:read orders:create',
'state' => $state,
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
// 'prompt' => '', // "none", "consent", "login" 중 선택
]);
return redirect('https://passport-app.test/oauth/authorize?'.$query);
});NOTE
state 값은 CSRF 공격을 방지하기 위한 임시 토큰입니다. 세션에 저장해 두었다가 콜백 단계에서 반드시 검증해야 합니다.
인가 코드를 액세스 토큰으로 교환
사용자가 인가 요청을 승인하면, Passport 서버는 클라이언트 애플리케이션의 콜백 URI로 인가 코드를 전달합니다. 이 시점에 세션에 저장해 둔 state 값과 콜백으로 전달된 state 파라미터가 일치하는지 먼저 검증합니다.
검증이 통과되면, 세션에서 code_verifier를 꺼내 인가 코드와 함께 Passport 서버에 POST 요청을 보내 액세스 토큰을 발급받습니다. 이때 클라이언트 시크릿은 필요하지 않습니다.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
Route::get('/callback', function (Request $request) {
$state = $request->session()->pull('state');
$codeVerifier = $request->session()->pull('code_verifier');
throw_unless(
strlen($state) > 0 && $state === $request->state,
InvalidArgumentException::class
);
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'authorization_code',
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'code_verifier' => $codeVerifier,
'code' => $request->code,
]);
return $response->json();
});Passport 서버는 수신한 code_verifier를 SHA-256으로 해시해 처음 전달받은 code_challenge와 비교합니다. 두 값이 일치하면 액세스 토큰이 정상 발급됩니다. 이 과정 덕분에 인가 코드가 탈취되더라도 code_verifier 없이는 토큰을 발급받을 수 없습니다.
Laravel Passport
디바이스 인증 그랜트 (Device Authorization Grant)
OAuth2 디바이스 인증 그랜트는 TV, 게임 콘솔처럼 브라우저가 없거나 입력 수단이 제한된 기기에서 "디바이스 코드"를 교환하여 액세스 토큰을 발급받을 수 있도록 하는 방식입니다.
이 흐름을 간략히 설명하면 다음과 같습니다.
즉, 기기는 사용자에게 URL과 코드를 보여주고, 사용자는 스마트폰이나 PC로 해당 URL에 접속해 코드를 입력하고 승인합니다. 기기는 그동안 주기적으로 서버를 폴링해 승인 여부를 확인합니다.
뷰 설정
Passport가 "사용자 코드 입력 화면"과 "승인/거부 화면"을 어떻게 렌더링할지 지정해야 합니다. App\Providers\AppServiceProvider의 boot 메서드에서 설정합니다.
use Inertia\Inertia;
use Laravel\Passport\Passport;
/**
* 애플리케이션 서비스 부트스트랩
*/
public function boot(): void
{
// 뷰 이름으로 지정하는 방법
Passport::deviceUserCodeView('auth.oauth.device.user-code');
Passport::deviceAuthorizationView('auth.oauth.device.authorize');
// 클로저로 지정하는 방법 (Inertia 등 사용 시)
Passport::deviceUserCodeView(
fn ($parameters) => Inertia::render('Auth/OAuth/Device/UserCode')
);
Passport::deviceAuthorizationView(
fn ($parameters) => Inertia::render('Auth/OAuth/Device/Authorize', [
'request' => $parameters['request'],
'authToken' => $parameters['authToken'],
'client' => $parameters['client'],
'user' => $parameters['user'],
'scopes' => $parameters['scopes'],
])
);
// ...
}Passport는 이 뷰를 반환하는 라우트를 자동으로 등록합니다.
각 뷰의 폼 요구사항
| 뷰 | 역할 | 필요한 폼 |
|---|---|---|
auth.oauth.device.user-code | 사용자 코드 입력 화면 | passport.device.authorizations.authorize 라우트로 GET 요청 (쿼리 파라미터: user_code) |
auth.oauth.device.authorize | 승인/거부 화면 | 승인: passport.device.authorizations.approve로 POST 요청 / 거부: passport.device.authorizations.deny로 DELETE 요청 (필드: state, client_id, auth_token) |
디바이스 인증 그랜트 클라이언트 생성
디바이스 인증 그랜트로 토큰을 발급하려면 먼저 디바이스 플로우가 활성화된 클라이언트를 생성해야 합니다.
Artisan 명령어로 생성 (1st-party 클라이언트)
php artisan passport:client --device명령어를 실행하면 클라이언트 ID와 시크릿이 출력됩니다.
코드로 생성 (3rd-party 클라이언트)
특정 사용자에게 속하는 서드파티 클라이언트는 ClientRepository를 사용해 등록할 수 있습니다.
use App\Models\User;
use Laravel\Passport\ClientRepository;
$user = User::find($userId);
$client = app(ClientRepository::class)->createDeviceAuthorizationGrantClient(
user: $user,
name: '스마트 TV 앱',
confidential: false, // 공개 클라이언트인 경우 false
);토큰 요청 흐름
1단계: 디바이스 코드 요청
클라이언트가 생성되면, 기기는 /oauth/device/code 엔드포인트에 POST 요청을 보내 디바이스 코드를 발급받습니다.
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://your-app.test/oauth/device/code', [
'client_id' => 'your-client-id',
'scope' => 'user:read orders:create',
]);
return $response->json();응답 예시:
{
"device_code": "...",
"user_code": "ABCD-1234",
"verification_uri": "https://your-app.test/oauth/device",
"expires_in": 900,
"interval": 5
}expires_in: 디바이스 코드의 유효 시간 (초)interval: 폴링 최소 간격 (초). 이 간격보다 짧게 폴링하면 속도 제한 오류가 발생할 수 있습니다.
NOTE
/oauth/device/code 라우트는 Passport가 자동으로 등록합니다. 별도로 정의할 필요가 없습니다.
2단계: 사용자에게 URL과 코드 안내
기기는 응답으로 받은 verification_uri와 user_code를 사용자에게 표시합니다. 사용자는 스마트폰이나 PC로 해당 URL에 접속해 코드를 입력하고 승인 또는 거부를 선택합니다.
3단계: 토큰 폴링
사용자가 별도 기기에서 승인/거부할 동안, 기기는 /oauth/token 엔드포인트를 주기적으로 폴링해 결과를 확인합니다.
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Sleep;
$interval = 5; // 디바이스 코드 응답의 interval 값 사용
do {
Sleep::for($interval)->seconds();
$response = Http::asForm()->post('https://your-app.test/oauth/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret', // 기밀 클라이언트(confidential)인 경우에만 필요
'device_code' => 'the-device-code',
]);
// slow_down 오류가 오면 폴링 간격을 늘려야 합니다
if ($response->json('error') === 'slow_down') {
$interval += 5;
}
} while (in_array($response->json('error'), ['authorization_pending', 'slow_down']));
return $response->json();폴링 중 발생할 수 있는 오류
| 오류 코드 | 의미 | 처리 방법 |
|---|---|---|
authorization_pending | 사용자가 아직 승인/거부하지 않음 | 폴링 계속 |
slow_down | 폴링 간격이 너무 짧음 | interval을 5초 증가시킨 후 계속 |
사용자가 승인을 완료하면 다음과 같은 JSON 응답이 반환됩니다.
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 3600
}expires_in: 액세스 토큰의 유효 시간 (초)
패스워드 그랜트
WARNING
패스워드 그랜트 토큰은 더 이상 권장하지 않습니다. 대신 OAuth2 Server에서 현재 권장하는 그랜트 타입을 사용하세요.
OAuth2 패스워드 그랜트는 모바일 앱과 같은 자사(first-party) 클라이언트가 이메일 주소(또는 사용자명)와 비밀번호를 이용해 액세스 토큰을 발급받을 수 있도록 합니다. 사용자가 OAuth2 인가 코드 리다이렉트 플로우를 거치지 않아도 되므로, 자사 클라이언트에 안전하게 토큰을 발급하는 데 유용합니다.
패스워드 그랜트를 활성화하려면 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 enablePasswordGrant 메서드를 호출하세요:
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::enablePasswordGrant();
}패스워드 그랜트 클라이언트 생성
패스워드 그랜트로 토큰을 발급하려면 먼저 패스워드 그랜트 클라이언트를 생성해야 합니다. passport:client Artisan 명령에 --password 옵션을 붙여 실행하세요:
php artisan passport:client --password토큰 요청
그랜트를 활성화하고 클라이언트를 생성했다면, 사용자의 이메일 주소와 비밀번호를 담아 /oauth/token 라우트로 POST 요청을 보내면 액세스 토큰을 발급받을 수 있습니다. 이 라우트는 Passport가 자동으로 등록하므로 별도로 정의할 필요가 없습니다. 요청이 성공하면 서버의 JSON 응답에서 access_token과 refresh_token을 받게 됩니다:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'password',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret', // 기밀 클라이언트(confidential client)에만 필요합니다...
'username' => 'taylor@laravel.com',
'password' => 'my-password',
'scope' => 'user:read orders:create',
]);
return $response->json();NOTE
액세스 토큰은 기본적으로 장기간 유효합니다. 필요하다면 최대 액세스 토큰 유효 기간 설정을 통해 조정할 수 있습니다.
모든 스코프 요청
패스워드 그랜트 또는 클라이언트 자격증명 그랜트를 사용할 때, 애플리케이션이 지원하는 모든 스코프에 대한 권한을 토큰에 부여하고 싶다면 * 스코프를 요청하면 됩니다. * 스코프가 지정된 토큰은 can 메서드를 호출할 때 항상 true를 반환합니다. 이 스코프는 password 또는 client_credentials 그랜트로 발급된 토큰에만 할당할 수 있습니다:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'password',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret', // 기밀 클라이언트(confidential client)에만 필요합니다...
'username' => 'taylor@laravel.com',
'password' => 'my-password',
'scope' => '*',
]);유저 프로바이더 커스터마이징
애플리케이션에서 여러 개의 인증 유저 프로바이더를 사용하는 경우, artisan passport:client --password 명령 실행 시 --provider 옵션을 추가해 패스워드 그랜트 클라이언트가 사용할 유저 프로바이더를 지정할 수 있습니다. 지정하는 프로바이더 이름은 config/auth.php 설정 파일에 정의된 유효한 프로바이더와 일치해야 합니다. 이후 미들웨어로 라우트를 보호하여 해당 가드의 프로바이더에 속한 사용자만 인가되도록 제한할 수 있습니다.
사용자명 필드 커스터마이징
패스워드 그랜트로 인증할 때 Passport는 기본적으로 인증 모델의 email 속성을 사용자명으로 사용합니다. 이 동작을 변경하려면 모델에 findForPassport 메서드를 정의하세요:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Bridge\Client;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable implements OAuthenticatable
{
use HasApiTokens, Notifiable;
/**
* 주어진 사용자명에 해당하는 유저 인스턴스를 반환합니다.
*/
public function findForPassport(string $username, Client $client): User
{
return $this->where('username', $username)->first();
}
}비밀번호 유효성 검사 커스터마이징
패스워드 그랜트로 인증할 때 Passport는 기본적으로 모델의 password 속성을 이용해 비밀번호를 검증합니다. 모델에 password 속성이 없거나 비밀번호 검증 로직을 직접 제어하고 싶다면, 모델에 validateForPassportPasswordGrant 메서드를 정의하세요:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable implements OAuthenticatable
{
use HasApiTokens, Notifiable;
/**
* Passport 패스워드 그랜트에서 사용자의 비밀번호를 검증합니다.
*/
public function validateForPassportPasswordGrant(string $password): bool
{
return Hash::check($password, $this->password);
}
}암묵적 그랜트 (Implicit Grant)
WARNING
암묵적 그랜트 토큰은 더 이상 권장하지 않습니다. 대신 OAuth2 Server에서 현재 권장하는 그랜트 타입을 사용하세요.
암묵적 그랜트는 인가 코드 그랜트와 유사하지만, 인가 코드를 교환하는 과정 없이 토큰이 클라이언트에 직접 반환된다는 점이 다릅니다. 클라이언트 자격 증명을 안전하게 보관하기 어려운 JavaScript 애플리케이션이나 모바일 앱에서 주로 사용됩니다. 이 그랜트를 활성화하려면 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 enableImplicitGrant 메서드를 호출하세요:
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::enableImplicitGrant();
}암묵적 그랜트를 통해 토큰을 발급하려면, 먼저 암묵적 그랜트 클라이언트를 생성해야 합니다. passport:client Artisan 명령에 --implicit 옵션을 붙여 실행하면 됩니다:
php artisan passport:client --implicit그랜트를 활성화하고 암묵적 클라이언트를 생성하면, 개발자는 클라이언트 ID를 사용해 애플리케이션에 액세스 토큰을 요청할 수 있습니다. 요청 애플리케이션은 아래와 같이 /oauth/authorize 라우트로 리다이렉트 요청을 보내야 합니다:
use Illuminate\Http\Request;
Route::get('/redirect', function (Request $request) {
$request->session()->put('state', $state = Str::random(40));
$query = http_build_query([
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'token',
'scope' => 'user:read orders:create',
'state' => $state,
// 'prompt' => '', // "none", "consent", "login" 중 하나
]);
return redirect('https://passport-app.test/oauth/authorize?'.$query);
});NOTE
/oauth/authorize 라우트는 Passport가 이미 정의하고 있습니다. 별도로 직접 정의할 필요가 없습니다.
클라이언트 자격증명 그랜트
클라이언트 자격증명 그랜트(Client Credentials Grant)는 머신-투-머신(M2M) 인증에 적합한 방식입니다. 예를 들어, 특정 API를 통해 정기적인 유지보수 작업을 수행하는 스케줄 Job에서 활용할 수 있습니다. 사용자가 직접 개입하지 않고, 클라이언트 애플리케이션 자체가 인증 주체가 됩니다.
이 그랜트 방식으로 토큰을 발급하려면 먼저 클라이언트 자격증명 그랜트용 클라이언트를 생성해야 합니다. passport:client Artisan 명령에 --client 옵션을 지정하여 생성합니다:
php artisan passport:client --client클라이언트를 생성했다면, 보호할 라우트에 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 미들웨어를 적용합니다:
use Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner;
Route::get('/orders', function (Request $request) {
// 액세스 토큰이 유효하고 클라이언트가 리소스 소유자입니다...
})->middleware(EnsureClientIsResourceOwner::class);특정 스코프를 가진 클라이언트만 라우트에 접근할 수 있도록 제한하려면, using 메서드에 필요한 스코프 목록을 전달하세요:
Route::get('/orders', function (Request $request) {
// 액세스 토큰이 유효하고, 클라이언트가 리소스 소유자이며,
// "servers:read"와 "servers:create" 스코프를 모두 보유한 경우...
})->middleware(EnsureClientIsResourceOwner::using('servers:read', 'servers:create'));WARNING
기반 OAuth2 서버는 클라이언트 자격증명 토큰의 sub 클레임 값으로 클라이언트의 식별자를 사용합니다. 기본적으로 Passport는 클라이언트에 UUID를 사용하므로, 사용자의 정수형 기본키와 충돌할 위험이 없습니다. 그러나 Passport::$clientUuids를 false로 설정한 경우, 클라이언트 ID와 동일한 ID를 가진 사용자가 의도치 않게 resolve될 수 있습니다. 이런 상황에서는 이 미들웨어가 수신된 토큰이 클라이언트 자격증명 토큰임을 보장할 수 없으므로 주의하세요.
토큰 발급 요청
이 그랜트 방식으로 토큰을 발급받으려면 oauth/token 엔드포인트에 다음과 같이 요청합니다:
use Illuminate\Support\Facades\Http;
$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
'grant_type' => 'client_credentials',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret',
'scope' => 'servers:read servers:create',
]);
return $response->json()['access_token'];응답으로 받은 access_token 값을 이후 API 요청의 Authorization: Bearer 헤더에 포함하여 사용하면 됩니다.
Laravel Passport
Personal Access Token (개인 액세스 토큰)
사용자가 일반적인 인증 코드 리다이렉트 흐름을 거치지 않고, 직접 자신에게 액세스 토큰을 발급하고 싶을 때가 있습니다. 애플리케이션 UI를 통해 사용자가 직접 토큰을 발급할 수 있도록 허용하면, API를 간단히 테스트하거나 토큰 발급 방식을 단순화하는 데 유용합니다.
NOTE
애플리케이션에서 Passport를 주로 개인 액세스 토큰 발급 용도로만 사용한다면, Laravel의 경량 공식 라이브러리인 Laravel Sanctum 사용을 고려해 보세요.
개인 액세스 클라이언트 생성
개인 액세스 토큰을 발급하기 전에 먼저 개인 액세스 클라이언트를 생성해야 합니다. passport:client Artisan 명령에 --personal 옵션을 붙여 실행하면 됩니다. 이미 passport:install 명령을 실행한 경우에는 이 단계를 건너뛰어도 됩니다.
php artisan passport:client --personal사용자 프로바이더 커스터마이징
애플리케이션에서 인증 사용자 프로바이더를 여러 개 사용하는 경우, 개인 액세스 클라이언트가 어떤 프로바이더를 사용할지 --provider 옵션으로 지정할 수 있습니다.
php artisan passport:client --personal --provider=users지정하는 프로바이더 이름은 config/auth.php에 정의된 유효한 프로바이더여야 합니다. 이후 미들웨어를 통한 라우트 보호를 설정하면, 해당 가드의 프로바이더에 속한 사용자만 인가할 수 있습니다.
개인 액세스 토큰 관리
개인 액세스 클라이언트를 생성했다면, App\Models\User 모델 인스턴스의 createToken 메서드를 사용해 특정 사용자에게 토큰을 발급할 수 있습니다. createToken의 첫 번째 인수는 토큰 이름이며, 두 번째 인수로 스코프 배열을 선택적으로 전달할 수 있습니다.
use App\Models\User;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;
$user = User::find($userId);
// 스코프 없이 토큰 발급
$token = $user->createToken('내 토큰')->accessToken;
// 특정 스코프를 지정하여 토큰 발급
$token = $user->createToken('내 토큰', ['user:read', 'orders:create'])->accessToken;
// 모든 스코프를 허용하는 토큰 발급
$token = $user->createToken('내 토큰', ['*'])->accessToken;
// 사용자의 유효한 개인 액세스 토큰 목록 조회
$tokens = $user->tokens()
->with('client')
->where('revoked', false)
->where('expires_at', '>', Date::now())
->get()
->filter(fn (Token $token) => $token->client->hasGrantType('personal_access'));Laravel Passport
라우트 보호
미들웨어를 통한 보호
Passport는 들어오는 요청의 액세스 토큰을 검증하는 인증 가드를 제공합니다. api 가드를 passport 드라이버로 설정하면, 유효한 액세스 토큰이 필요한 라우트에 auth:api 미들웨어를 지정하기만 하면 됩니다.
Route::get('/user', function () {
// 유효한 API 액세스 토큰을 가진 사용자만 접근 가능합니다...
})->middleware('auth:api');WARNING
클라이언트 자격증명 그랜트(client credentials grant)를 사용하는 경우에는 auth:api 미들웨어 대신 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 미들웨어를 사용해 라우트를 보호해야 합니다.
여러 인증 가드 사용
애플리케이션에 서로 다른 Eloquent 모델을 사용하는 여러 유형의 사용자가 있다면, 각 유저 프로바이더 유형마다 별도의 가드 설정을 정의해야 할 수 있습니다. 이렇게 하면 특정 유저 프로바이더를 대상으로 하는 요청을 분리하여 보호할 수 있습니다.
예를 들어, config/auth.php에 다음과 같이 가드를 설정했다고 가정해 봅니다.
'guards' => [
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
'api-customers' => [
'driver' => 'passport',
'provider' => 'customers',
],
],아래 라우트는 api-customers 가드를 사용하여 customers 유저 프로바이더로 들어오는 요청을 인증합니다.
Route::get('/customer', function () {
// ...
})->middleware('auth:api-customers');NOTE
Passport에서 여러 유저 프로바이더를 함께 사용하는 방법에 대한 자세한 내용은 개인 액세스 토큰 문서와 패스워드 그랜트 문서를 참고하세요.
액세스 토큰 전달
Passport로 보호된 라우트를 호출할 때, API 사용자는 요청의 Authorization 헤더에 액세스 토큰을 Bearer 형식으로 포함해야 합니다. 예를 들어, Http 파사드를 사용하는 경우 다음과 같이 작성합니다.
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'Accept' => 'application/json',
'Authorization' => "Bearer $accessToken",
])->get('https://passport-app.test/api/user');
return $response->json();토큰 스코프
스코프(Scope)는 API 클라이언트가 계정 접근 권한을 요청할 때 어떤 작업을 허용할지 세밀하게 제한하는 기능입니다. 예를 들어 이커머스 애플리케이션을 만든다면, 모든 API 소비자가 주문 생성 권한까지 필요하지는 않을 것입니다. 어떤 소비자에게는 배송 상태 조회만 허용하면 충분합니다. 이처럼 스코프를 사용하면 제3자 애플리케이션이 사용자를 대신해 수행할 수 있는 작업 범위를 명확하게 통제할 수 있습니다.
스코프 정의
API 스코프는 App\Providers\AppServiceProvider 클래스의 boot 메서드에서 Passport::tokensCan 메서드를 사용해 정의합니다. tokensCan 메서드는 스코프 이름을 키로, 스코프 설명을 값으로 갖는 배열을 받습니다. 스코프 설명은 사용자가 권한 승인 화면에서 보게 될 텍스트이므로, 사용자가 이해하기 쉽게 작성하는 것이 좋습니다.
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::tokensCan([
'user:read' => '사용자 정보 조회',
'orders:create' => '주문 생성',
'orders:read:status' => '주문 배송 상태 조회',
]);
}기본 스코프
클라이언트가 별도의 스코프를 요청하지 않는 경우, defaultScopes 메서드를 사용해 토큰에 자동으로 부여할 기본 스코프를 설정할 수 있습니다. 이 메서드도 마찬가지로 AppServiceProvider의 boot 메서드에서 호출합니다.
use Laravel\Passport\Passport;
Passport::tokensCan([
'user:read' => '사용자 정보 조회',
'orders:create' => '주문 생성',
'orders:read:status' => '주문 배송 상태 조회',
]);
Passport::defaultScopes([
'user:read',
'orders:create',
]);토큰에 스코프 할당
인가 코드 요청 시
인가 코드 방식으로 액세스 토큰을 요청할 때는, scope 쿼리 파라미터에 원하는 스코프를 공백으로 구분해 지정합니다.
Route::get('/redirect', function () {
$query = http_build_query([
'client_id' => 'your-client-id',
'redirect_uri' => 'https://third-party-app.com/callback',
'response_type' => 'code',
'scope' => 'user:read orders:create',
]);
return redirect('https://passport-app.test/oauth/authorize?'.$query);
});개인 액세스 토큰 발급 시
App\Models\User 모델의 createToken 메서드로 개인 액세스 토큰을 발급할 때는, 두 번째 인수로 스코프 배열을 전달합니다.
$token = $user->createToken('내 토큰', ['orders:create'])->accessToken;스코프 확인
Passport는 들어오는 요청의 토큰이 특정 스코프를 보유하고 있는지 검증하는 두 가지 미들웨어를 제공합니다.
모든 스코프 보유 여부 확인
Laravel\Passport\Http\Middleware\CheckToken 미들웨어는 액세스 토큰이 지정한 스코프를 모두 보유하고 있는지 확인합니다. 하나라도 없으면 요청이 거부됩니다.
use Laravel\Passport\Http\Middleware\CheckToken;
Route::get('/orders', function () {
// 액세스 토큰에 "orders:read"와 "orders:create" 스코프가 모두 있어야 합니다.
})->middleware(['auth:api', CheckToken::using('orders:read', 'orders:create')]);스코프 중 하나 이상 보유 여부 확인
Laravel\Passport\Http\Middleware\CheckTokenForAnyScope 미들웨어는 액세스 토큰이 지정한 스코프 중 하나 이상을 보유하고 있는지 확인합니다.
use Laravel\Passport\Http\Middleware\CheckTokenForAnyScope;
Route::get('/orders', function () {
// 액세스 토큰에 "orders:read" 또는 "orders:create" 중 하나 이상이 있어야 합니다.
})->middleware(['auth:api', CheckTokenForAnyScope::using('orders:read', 'orders:create')]);토큰 인스턴스에서 스코프 확인
액세스 토큰으로 인증된 요청이 애플리케이션에 들어온 이후에도, 인증된 App\Models\User 인스턴스의 tokenCan 메서드를 사용해 토큰이 특정 스코프를 가지고 있는지 추가로 확인할 수 있습니다.
use Illuminate\Http\Request;
Route::get('/orders', function (Request $request) {
if ($request->user()->tokenCan('orders:create')) {
// 주문 생성 권한이 있을 때의 처리...
}
});스코프 관련 추가 메서드
정의된 모든 스코프의 ID(이름) 목록을 배열로 가져오려면 scopeIds 메서드를 사용합니다.
use Laravel\Passport\Passport;
Passport::scopeIds();정의된 모든 스코프를 Laravel\Passport\Scope 인스턴스 배열로 가져오려면 scopes 메서드를 사용합니다.
Passport::scopes();특정 ID(이름)에 해당하는 스코프만 Laravel\Passport\Scope 인스턴스 배열로 가져오려면 scopesFor 메서드를 사용합니다.
Passport::scopesFor(['user:read', 'orders:create']);특정 스코프가 정의되어 있는지 확인하려면 hasScope 메서드를 사용합니다.
Passport::hasScope('orders:create');SPA 인증
JavaScript 애플리케이션에서 자신이 만든 API를 직접 호출해야 하는 경우가 많습니다. 예를 들어 Vue나 React 기반의 SPA(Single Page Application)를 Laravel 백엔드와 함께 운영할 때, 외부에 공개한 것과 동일한 API를 자사 프론트엔드에서도 그대로 활용할 수 있습니다. 이 구조는 웹 앱, 모바일 앱, 서드파티 클라이언트, SDK 등 모든 클라이언트가 단일 API를 공유할 수 있다는 장점이 있습니다.
일반적으로 JavaScript 애플리케이션에서 API를 호출하려면 액세스 토큰을 직접 관리하고 매 요청마다 헤더에 담아 보내야 합니다. 하지만 Passport는 이 과정을 자동으로 처리해주는 미들웨어를 제공합니다. bootstrap/app.php 파일의 web 미들웨어 그룹에 CreateFreshApiToken 미들웨어를 추가하기만 하면 됩니다:
use Laravel\Passport\Http\Middleware\CreateFreshApiToken;
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [
CreateFreshApiToken::class,
]);
})WARNING
CreateFreshApiToken 미들웨어는 반드시 미들웨어 스택의 맨 마지막에 위치해야 합니다.
이 미들웨어는 서버에서 응답을 보낼 때 laravel_token이라는 쿠키를 자동으로 첨부합니다. 이 쿠키에는 암호화된 JWT가 담겨 있으며, Passport는 이를 통해 JavaScript 클라이언트의 API 요청을 인증합니다. JWT의 유효 기간은 session.lifetime 설정값과 동일합니다.
브라우저는 이후 모든 요청에 이 쿠키를 자동으로 포함시키므로, 별도로 액세스 토큰을 전달하지 않아도 API를 호출할 수 있습니다:
axios.get('/api/user')
.then(response => {
console.log(response.data);
});쿠키 이름 변경
필요에 따라 laravel_token 쿠키의 이름을 변경할 수 있습니다. App\Providers\AppServiceProvider 클래스의 boot 메서드에서 Passport::cookie 메서드를 호출하면 됩니다:
/**
* 애플리케이션 서비스를 부트스트랩합니다.
*/
public function boot(): void
{
Passport::cookie('custom_name');
}CSRF 보호
이 인증 방식을 사용할 때는 모든 요청에 유효한 CSRF 토큰 헤더가 포함되어야 합니다. Laravel 기본 스캐폴딩 및 스타터 킷에는 Axios 인스턴스가 포함되어 있으며, 이 인스턴스는 동일 출처(same-origin) 요청 시 암호화된 XSRF-TOKEN 쿠키 값을 읽어 X-XSRF-TOKEN 헤더를 자동으로 전송합니다.
NOTE
X-XSRF-TOKEN 대신 X-CSRF-TOKEN 헤더를 직접 전송하려는 경우, csrf_token() 함수가 반환하는 암호화되지 않은 토큰을 사용해야 합니다.
Laravel Passport
이벤트
Passport는 액세스 토큰과 리프레시 토큰을 발급할 때 이벤트를 발생시킵니다. 이 이벤트를 리스닝하면 데이터베이스에서 다른 액세스 토큰을 정리하거나 취소하는 작업을 처리할 수 있습니다.
| 이벤트 이름 |
|---|
Laravel\Passport\Events\AccessTokenCreated |
Laravel\Passport\Events\AccessTokenRevoked |
Laravel\Passport\Events\RefreshTokenCreated |
예를 들어, 새로운 액세스 토큰이 발급될 때 해당 사용자의 기존 토큰을 자동으로 폐기하고 싶다면 AccessTokenCreated 이벤트에 리스너를 등록하면 됩니다.
namespace App\Listeners;
use Laravel\Passport\Events\AccessTokenCreated;
class RevokeOldTokens
{
public function handle(AccessTokenCreated $event): void
{
// $event->tokenId — 새로 생성된 토큰 ID
// $event->userId — 해당 사용자 ID
// $event->clientId — 해당 클라이언트 ID
}
}NOTE
이벤트 리스너를 등록하는 방법은 이벤트 문서를 참고하세요.
Laravel Passport — 테스트
테스트
Passport는 테스트 환경에서 인증된 사용자나 클라이언트를 손쉽게 시뮬레이션할 수 있도록 전용 헬퍼 메서드를 제공합니다. 실제 OAuth 토큰 발급 과정 없이도 보호된 API 엔드포인트를 테스트할 수 있어 편리합니다.
사용자로 인증하기
Passport::actingAs 메서드를 사용하면 현재 인증된 사용자와 해당 토큰에 부여할 스코프를 지정할 수 있습니다. 첫 번째 인수로 사용자 인스턴스를, 두 번째 인수로 부여할 스코프 배열을 전달합니다.
Pest
use App\Models\User;
use Laravel\Passport\Passport;
test('주문을 생성할 수 있다', function () {
Passport::actingAs(
User::factory()->create(),
['orders:create']
);
$response = $this->post('/api/orders');
$response->assertStatus(201);
});PHPUnit
use App\Models\User;
use Laravel\Passport\Passport;
public function test_orders_can_be_created(): void
{
Passport::actingAs(
User::factory()->create(),
['orders:create']
);
$response = $this->post('/api/orders');
$response->assertStatus(201);
}클라이언트로 인증하기
클라이언트 자격증명 그랜트(Client Credentials Grant)처럼 사용자가 아닌 클라이언트 자체가 인증 주체가 되는 경우에는 Passport::actingAsClient 메서드를 사용합니다. 첫 번째 인수로 클라이언트 인스턴스를, 두 번째 인수로 부여할 스코프 배열을 전달합니다.
Pest
use Laravel\Passport\Client;
use Laravel\Passport\Passport;
test('서버 목록을 조회할 수 있다', function () {
Passport::actingAsClient(
Client::factory()->create(),
['servers:read']
);
$response = $this->get('/api/servers');
$response->assertStatus(200);
});PHPUnit
use Laravel\Passport\Client;
use Laravel\Passport\Passport;
public function test_servers_can_be_retrieved(): void
{
Passport::actingAsClient(
Client::factory()->create(),
['servers:read']
);
$response = $this->get('/api/servers');
$response->assertStatus(200);
}NOTE
스코프 배열을 빈 배열([])로 전달하면 모든 스코프 검사를 통과하는 토큰이 생성됩니다. 테스트 목적에 맞게 필요한 스코프만 명시적으로 지정하는 것을 권장합니다.