> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 커스텀 인증 가드 구현

> Laravel 인증 시스템의 내부 구조를 이해하고, Guard·StatefulGuard 인터페이스를 구현하여 커스텀 인증 가드를 만드는 방법을 프레임워크 소스 코드 수준에서 설명합니다.

## Laravel 인증 시스템의 내부 구조

### `Auth` 파사드와 `AuthManager`

`Auth` 파사드는 `Illuminate\Auth\AuthManager`의 프록시입니다. `AuthManager`는 드라이버 패턴으로 여러 가드를 관리하며, `config/auth.php` 설정을 기반으로 적절한 가드 인스턴스를 생성하고 캐시합니다.

```php theme={null}
// Auth::guard('web')의 내부 동작(AuthManager::guard()의 간략한 버전)
public function guard($name = null)
{
    $name = $name ?: $this->getDefaultDriver();

    return $this->guards[$name] ?? ($this->guards[$name] = $this->resolve($name));
}
```

`resolve()`는 `config/auth.php`의 `guards` 배열에서 `driver` 키를 읽어 대응하는 팩토리 클로저를 호출합니다. 기본 제공되는 `session`이나 `token` 드라이버도 동일한 구조로 등록되어 있습니다.

### `Guard` 인터페이스와 `StatefulGuard` 인터페이스의 차이

Laravel 인증 가드는 최소한 `Illuminate\Contracts\Auth\Guard`를 구현해야 합니다. 세션을 유지해야 하는 경우에는 `StatefulGuard`를 구현합니다.

<Accordion title="Guard 인터페이스(Illuminate\Contracts\Auth\Guard)">
  ```php theme={null}
  interface Guard
  {
      // 인증된 사용자가 존재하는지 확인
      public function check();

      // 게스트(미인증) 여부 확인
      public function guest();

      // 현재 인증된 사용자를 반환(미인증이면 null)
      public function user();

      // 현재 인증된 사용자의 ID를 반환
      public function id();

      // 주어진 자격 증명이 유효한지 확인(로그인은 수행하지 않음)
      public function validate(array $credentials = []);

      // 사용자가 설정되어 있는지 확인(미로그인 상태에서도 setUser로 주입 가능)
      public function hasUser();

      // 인증된 사용자를 수동으로 설정
      public function setUser(Authenticatable $user);
  }
  ```
</Accordion>

<Accordion title="StatefulGuard 인터페이스(Illuminate\Contracts\Auth\StatefulGuard)">
  `StatefulGuard`는 `Guard`를 상속하며, 세션이나 쿠키를 사용한 로그인 상태 유지에 필요한 메서드를 추가합니다.

  ```php theme={null}
  interface StatefulGuard extends Guard
  {
      // 자격 증명을 검증하고 로그인(remember 플래그 포함)
      public function attempt(array $credentials = [], $remember = false);

      // 세션을 저장하지 않고 1개의 요청에서만 인증
      public function once(array $credentials = []);

      // 사용자 인스턴스를 직접 로그인
      public function login(Authenticatable $user, $remember = false);

      // ID를 지정하여 로그인
      public function loginUsingId($id, $remember = false);

      // ID를 지정하여 1개의 요청에서만 인증
      public function onceUsingId($id);

      // "로그인 상태 유지" 쿠키로 로그인했는지 확인
      public function viaRemember();

      // 로그아웃
      public function logout();
  }
  ```
</Accordion>

<Info>
  API 인증이나 자체 토큰 인증처럼 세션이 필요하지 않은 가드는 `Guard`만 구현하면 됩니다. 관리자 로그인처럼 세션이 필요한 경우에는 `StatefulGuard`를 구현합니다.
</Info>

## 커스텀 가드 구현

### `GuardHelpers` 트레이트

`Guard` 인터페이스의 `check()`, `guest()`, `id()`, `hasUser()`는 대부분 공통된 구현이 되기 때문에, Laravel은 `Illuminate\Auth\GuardHelpers` 트레이트를 제공합니다. 이 트레이트를 사용하면 필수 구현을 `user()`와 `validate()` 두 메서드로 줄일 수 있습니다.

```php theme={null}
// GuardHelpers가 제공하는 기본 구현(발췌)
trait GuardHelpers
{
    protected $user;

    public function check(): bool
    {
        return ! is_null($this->user());
    }

    public function guest(): bool
    {
        return ! $this->check();
    }

    public function id(): mixed
    {
        return $this->user()?->getAuthIdentifier();
    }

    public function hasUser(): bool
    {
        return ! is_null($this->user);
    }

    public function setUser(Authenticatable $user): static
    {
        $this->user = $user;
        return $this;
    }
}
```

### API 토큰 인증 가드 구현 예제

`TokenGuard`의 설계를 참고하여 간단한 API 토큰 인증 가드를 구현합니다. 요청 헤더 또는 쿼리 파라미터에서 토큰을 가져와 `UserProvider`를 통해 사용자를 해석합니다.

<Steps>
  <Step title="가드 클래스 생성">
    `app/Auth` 디렉터리에 가드 클래스를 생성합니다.

    ```php theme={null}
    <?php

    namespace App\Auth;

    use Illuminate\Auth\GuardHelpers;
    use Illuminate\Contracts\Auth\Guard;
    use Illuminate\Contracts\Auth\UserProvider;
    use Illuminate\Http\Request;

    class ApiTokenGuard implements Guard
    {
        use GuardHelpers;

        protected Request $request;

        public function __construct(UserProvider $provider, Request $request)
        {
            $this->provider = $provider;
            $this->request = $request;
        }

        /**
         * 현재 인증된 사용자를 반환
         */
        public function user(): ?\Illuminate\Contracts\Auth\Authenticatable
        {
            // 캐시된 사용자가 있다면 반환
            if (! is_null($this->user)) {
                return $this->user;
            }

            $token = $this->getTokenForRequest();

            if (empty($token)) {
                return null;
            }

            // UserProvider를 통해 사용자 조회
            $this->user = $this->provider->retrieveByCredentials([
                'api_token' => $token,
            ]);

            return $this->user;
        }

        /**
         * 자격 증명 검증만 수행(로그인은 하지 않음)
         */
        public function validate(array $credentials = []): bool
        {
            if (empty($credentials['api_token'])) {
                return false;
            }

            return (bool) $this->provider->retrieveByCredentials($credentials);
        }

        /**
         * 요청에서 토큰을 가져옴
         *
         * 우선순위: Bearer 헤더 → 쿼리 파라미터 → 요청 바디
         */
        protected function getTokenForRequest(): ?string
        {
            $token = $this->request->bearerToken();

            if (empty($token)) {
                $token = $this->request->query('api_token');
            }

            if (empty($token)) {
                $token = $this->request->input('api_token');
            }

            return $token ?: null;
        }

        /**
         * 요청 인스턴스를 교체
         */
        public function setRequest(Request $request): static
        {
            $this->request = $request;
            return $this;
        }
    }
    ```
  </Step>

  <Step title="서비스 프로바이더에서 가드 등록">
    `AppServiceProvider`의 `boot()` 메서드에서 `Auth::extend()`를 사용해 가드를 등록합니다.

    ```php theme={null}
    <?php

    namespace App\Providers;

    use App\Auth\ApiTokenGuard;
    use Illuminate\Contracts\Foundation\Application;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\ServiceProvider;

    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            Auth::extend('api-token', function (Application $app, string $name, array $config) {
                // Auth::createUserProvider()로 config의 provider를 해석
                $provider = Auth::createUserProvider($config['provider'] ?? 'users');

                return new ApiTokenGuard($provider, $app->make('request'));
            });
        }
    }
    ```

    <Info>
      `Auth::createUserProvider()`는 `config/auth.php`의 `providers` 설정을 읽어 해당하는 `UserProvider` 인스턴스를 반환합니다. 커스텀 프로바이더를 만들지 않는 한, 이 호출 방식으로 기본 `EloquentUserProvider`를 사용할 수 있습니다.
    </Info>
  </Step>

  <Step title="config/auth.php에서 가드 설정">
    `config/auth.php`에 새로운 가드를 추가합니다.

    ```php theme={null}
    'guards' => [
        'web' => [
            'driver'   => 'session',
            'provider' => 'users',
        ],

        // 추가한 커스텀 가드
        'api' => [
            'driver'   => 'api-token', // Auth::extend()의 첫 번째 인자와 일치시킴
            'provider' => 'users',
        ],
    ],
    ```
  </Step>

  <Step title="라우트에 가드 적용">
    `auth` 미들웨어에 가드 이름을 지정합니다.

    ```php theme={null}
    // routes/api.php
    use Illuminate\Support\Facades\Route;

    Route::middleware('auth:api')->group(function () {
        Route::get('/user', function () {
            return auth()->user();
        });

        Route::get('/posts', [\App\Http\Controllers\PostController::class, 'index']);
    });
    ```

    컨트롤러나 코드 내에서 특정 가드를 사용하려면 `Auth::guard('api')` 또는 `auth('api')`를 호출합니다.

    ```php theme={null}
    $user = Auth::guard('api')->user();
    ```
  </Step>
</Steps>

## 클로저를 이용한 간단한 가드

`Auth::viaRequest()`를 사용하면 클래스를 만들지 않고 클로저만으로 간단한 가드를 정의할 수 있습니다. 프로토타입이나 매우 단순한 인증에 적합합니다.

```php theme={null}
use Illuminate\Http\Request;
use App\Models\User;

// AppServiceProvider::boot() 내부
Auth::viaRequest('custom-token', function (Request $request): ?User {
    $token = $request->bearerToken();

    if (empty($token)) {
        return null;
    }

    return User::where('api_token', $token)->first();
});
```

`config/auth.php` 설정:

```php theme={null}
'guards' => [
    'api' => [
        'driver' => 'custom-token',
    ],
],
```

<Warning>
  `Auth::viaRequest()`로 정의한 가드는 `UserProvider`를 사용하지 않기 때문에 `retrieveById()`와 같은 프로바이더 메서드가 작동하지 않습니다. 프로덕션 환경에서는 `Auth::extend()`를 사용하는 클래스 기반 가드를 권장합니다.
</Warning>

## 커스텀 `UserProvider` 구현

사용자 정보를 데이터베이스가 아닌 다른 소스(외부 API, LDAP 등)에서 가져올 경우, `Illuminate\Contracts\Auth\UserProvider` 인터페이스를 구현합니다.

```php theme={null}
<?php

namespace App\Auth;

use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;

class ApiUserProvider implements UserProvider
{
    public function __construct(
        protected string $apiBaseUrl,
        protected string $model = User::class,
    ) {}

    /**
     * ID로 사용자 조회
     */
    public function retrieveById(mixed $identifier): ?Authenticatable
    {
        return ($this->model)::find($identifier);
    }

    /**
     * "로그인 상태 유지" 토큰으로 사용자 조회
     * 세션을 사용하지 않는 가드에서는 null을 반환하기만 하면 됨
     */
    public function retrieveByToken(mixed $identifier, string $token): ?Authenticatable
    {
        return null;
    }

    /**
     * "로그인 상태 유지" 토큰 갱신
     * 세션을 사용하지 않는 가드에서는 아무것도 하지 않아도 됨
     */
    public function updateRememberToken(Authenticatable $user, string $token): void {}

    /**
     * 자격 증명으로 사용자 조회
     * 가드의 validate()와 user()에서 호출됨
     */
    public function retrieveByCredentials(array $credentials): ?Authenticatable
    {
        if (empty($credentials['api_token'])) {
            return null;
        }

        // 외부 API에서 토큰을 검증하고 사용자 정보를 가져오는 예제
        $response = \Illuminate\Support\Facades\Http::withToken($credentials['api_token'])
            ->get("{$this->apiBaseUrl}/auth/me");

        if (! $response->successful()) {
            return null;
        }

        $data = $response->json();

        // 로컬 DB의 사용자와 연동하거나 동적으로 모델 생성
        return User::firstOrCreate(
            ['external_id' => $data['id']],
            ['name' => $data['name'], 'email' => $data['email']],
        );
    }

    /**
     * 조회한 사용자의 자격 증명을 검증
     * 토큰 인증에서는 retrieveByCredentials가 성공하면 true로 충분
     */
    public function validateCredentials(Authenticatable $user, array $credentials): bool
    {
        return true;
    }

    /**
     * 비밀번호 재해시가 필요한지 확인
     * 토큰 인증에서는 항상 false로 둠
     */
    public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void {}
}
```

### 커스텀 `UserProvider` 등록

```php theme={null}
// AppServiceProvider::boot()
Auth::provider('api-user', function (Application $app, array $config) {
    return new \App\Auth\ApiUserProvider(
        config('services.auth_api.base_url'),
        $config['model'] ?? \App\Models\User::class,
    );
});
```

`config/auth.php`의 `providers` 섹션에 추가합니다:

```php theme={null}
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model'  => App\Models\User::class,
    ],

    // 커스텀 프로바이더
    'api-users' => [
        'driver' => 'api-user',
        'model'  => App\Models\User::class,
    ],
],
```

가드와 프로바이더를 조합합니다:

```php theme={null}
'guards' => [
    'api' => [
        'driver'   => 'api-token',
        'provider' => 'api-users', // 커스텀 프로바이더 지정
    ],
],
```

## 실전 유스케이스

### 멀티 인증(관리자와 일반 사용자에 별도 가드)

<Steps>
  <Step title="관리자 모델 생성">
    관리자용 Eloquent 모델을 준비합니다. `Authenticatable`을 상속함으로써 `Auth` 시스템과 연동할 수 있습니다.

    ```bash theme={null}
    php artisan make:model Admin -m
    ```

    ```php theme={null}
    <?php

    namespace App\Models;

    use Illuminate\Foundation\Auth\User as Authenticatable;

    class Admin extends Authenticatable
    {
        protected $fillable = ['name', 'email', 'password'];

        protected $hidden = ['password', 'remember_token'];
    }
    ```
  </Step>

  <Step title="config/auth.php 설정">
    ```php theme={null}
    'guards' => [
        'web' => [
            'driver'   => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver'   => 'session',
            'provider' => 'admins', // 관리자용 프로바이더
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model'  => App\Models\User::class,
        ],
        'admins' => [
            'driver' => 'eloquent',
            'model'  => App\Models\Admin::class, // 관리자 모델
        ],
    ],
    ```
  </Step>

  <Step title="라우트와 미들웨어 설정">
    ```php theme={null}
    // routes/web.php

    // 일반 사용자용 라우트(기본 web 가드)
    Route::middleware('auth')->group(function () {
        Route::get('/dashboard', [DashboardController::class, 'index']);
    });

    // 관리자용 라우트(admin 가드)
    Route::prefix('admin')->middleware('auth:admin')->group(function () {
        Route::get('/dashboard', [AdminDashboardController::class, 'index']);
    });
    ```
  </Step>

  <Step title="가드를 지정한 로그인 처리 작성">
    ```php theme={null}
    <?php

    namespace App\Http\Controllers\Admin;

    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;

    class LoginController extends Controller
    {
        public function store(Request $request)
        {
            $credentials = $request->validate([
                'email'    => 'required|email',
                'password' => 'required',
            ]);

            // admin 가드를 명시하여 attempt 수행
            if (Auth::guard('admin')->attempt($credentials)) {
                $request->session()->regenerate();
                return redirect()->intended('/admin/dashboard');
            }

            return back()->withErrors(['email' => '로그인 정보가 올바르지 않습니다.']);
        }

        public function destroy(Request $request)
        {
            Auth::guard('admin')->logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect('/admin/login');
        }
    }
    ```
  </Step>
</Steps>

### JWT 토큰을 이용한 외부 API 인증

외부 JWT 인증 서비스를 사용하는 경우의 커스텀 가드 구현 예제입니다.

```php theme={null}
<?php

namespace App\Auth;

use App\Models\User;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class JwtGuard implements Guard
{
    use GuardHelpers;

    protected Request $request;
    protected ?array $payload = null;

    public function __construct(UserProvider $provider, Request $request)
    {
        $this->provider = $provider;
        $this->request  = $request;
    }

    public function user(): ?\Illuminate\Contracts\Auth\Authenticatable
    {
        if (! is_null($this->user)) {
            return $this->user;
        }

        $token = $this->request->bearerToken();

        if (empty($token)) {
            return null;
        }

        // 외부 서비스에서 JWT 검증
        $payload = $this->verifyToken($token);

        if (is_null($payload)) {
            return null;
        }

        $this->payload = $payload;

        $this->user = $this->provider->retrieveById($payload['sub']);

        return $this->user;
    }

    public function validate(array $credentials = []): bool
    {
        if (empty($credentials['token'])) {
            return false;
        }

        return ! is_null($this->verifyToken($credentials['token']));
    }

    /**
     * JWT 검증 및 payload 획득
     */
    protected function verifyToken(string $token): ?array
    {
        $response = Http::withToken($token)
            ->get(config('services.auth.verify_url'));

        if (! $response->successful()) {
            return null;
        }

        return $response->json();
    }

    /**
     * 검증된 JWT payload를 반환
     */
    public function payload(): ?array
    {
        return $this->payload;
    }
}
```

`AppServiceProvider`에서 등록:

```php theme={null}
Auth::extend('jwt', function (Application $app, string $name, array $config) {
    return new \App\Auth\JwtGuard(
        Auth::createUserProvider($config['provider'] ?? 'users'),
        $app->make('request'),
    );
});
```

<Tip>
  `Auth::guard('jwt')->payload()`처럼 커스텀 가드 고유의 메서드에도 접근할 수 있습니다. `Auth::guard()`가 반환하는 것은 가드 인스턴스 그 자체이므로, 인터페이스에 없는 메서드도 호출할 수 있습니다.
</Tip>

## 테스트

커스텀 가드의 유닛 테스트에서는 `UserProvider`를 목(mock)으로 만들어 가드의 동작을 확인합니다.

```php theme={null}
<?php

namespace Tests\Unit\Auth;

use App\Auth\ApiTokenGuard;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;

class ApiTokenGuardTest extends TestCase
{
    private UserProvider&MockObject $provider;
    private ApiTokenGuard $guard;

    protected function setUp(): void
    {
        parent::setUp();

        $this->provider = $this->createMock(UserProvider::class);
    }

    public function test_user_returns_null_when_no_token(): void
    {
        $request = Request::create('/');
        $guard   = new ApiTokenGuard($this->provider, $request);

        $this->assertNull($guard->user());
    }

    public function test_user_returns_user_with_valid_bearer_token(): void
    {
        $mockUser = $this->createMock(Authenticatable::class);

        $this->provider
            ->expects($this->once())
            ->method('retrieveByCredentials')
            ->with(['api_token' => 'valid-token'])
            ->willReturn($mockUser);

        $request = Request::create('/');
        $request->headers->set('Authorization', 'Bearer valid-token');

        $guard = new ApiTokenGuard($this->provider, $request);

        $this->assertSame($mockUser, $guard->user());
    }

    public function test_check_returns_false_when_no_token(): void
    {
        $request = Request::create('/');
        $guard   = new ApiTokenGuard($this->provider, $request);

        $this->assertFalse($guard->check());
    }

    public function test_validate_returns_false_without_api_token_credential(): void
    {
        $request = Request::create('/');
        $guard   = new ApiTokenGuard($this->provider, $request);

        $this->assertFalse($guard->validate([]));
    }
}
```

`ActingAs`를 사용한 기능 테스트에서는 특정 가드에 사용자를 설정할 수 있습니다.

```php theme={null}
// 특정 가드로 사용자를 인증하여 테스트
$this->actingAs($user, 'api')
    ->getJson('/api/user')
    ->assertOk();
```

## 관련 페이지

<Card title="인증(입문)" icon="shield-check" href="/ko/authentication">
  스타터 킷과 표준 인증 흐름을 확인합니다.
</Card>

<Card title="서비스 컨테이너" icon="box" href="/ko/service-container">
  가드 등록에서 사용하는 서비스 컨테이너의 구조를 이해합니다.
</Card>


## Related topics

- [GeneratorCommand — 커스텀 make: 명령어 구현](/ko/advanced/generator-command.md)
- [Eloquent의 커스텀 캐스트](/ko/advanced/eloquent-casts.md)
- [Laravel에서 MCP 서버 만들기](/ko/advanced/mcp-server.md)
- [에러 핸들링](/ko/error-handling.md)
- [Laravel Fortify와 스타터 킷](/ko/advanced/fortify.md)
