LaravelでシンプルなLDAP認証をシンプルに作る

# 概要
Laravel関連の記事がややこしすぎるのでシンプルに書きたい。
Guardがなんじゃとか、Providerがどうのとか、そういった解説はせずに、
シンプルにコード類をまとめておきます。

- Miracle Linux 9
- php 8.0
- Laravel 9.52
- LDAPサーバー → ActiveDirectory

以下、今回のコードで作る仕組みの概略図です。
LDAP認証でログインできるシンプルなログイン画面とログイン後のページを実装します。

**Webルーティング概要**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2420694/e715c49a-78fb-ac58-00e9-727088d44d77.png)

**LDAP認証周りの繋がり**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2420694/c581f2d6-6483-d5ae-2e1d-a653217887ce.png)


# 作る必要があるもの
- [ログイン画面ログイン後の画面の表示](##ログイン画面ログイン後の画面の表示)
    - htmlの作成（resources/views配下）
        - auth/login.blade.php ... ログイン画面
        - dashboard.blade.php ... ログイン後の画面
    - 対応コントローラーの作成
        - AuthController...ログイン画面とログイン処理
        - DashboardContloller... ログイン後の画面
    - Webルーティングの設定
        - routes/web.php
- LDAP認証周りの仕組み作成
    - LDAP認証ユーザー用のユーザーモデルを作成
        - app/Models/LdapUser.php
    - LDAP認証用のプロバイダーを作成
        - app/Providers/LdapUserProvider.php
    - 独自プロパイダ－を認識するように設定
        - app/Providers/AuthServiceProvider.php
    - 認証設定ファイルを更新
        - config/auth.php
    - 環境変数の設定
        - .env

# 詳細
## ログイン画面・ログイン後の画面の表示
### htmlの作成（resources/views配下）
それぞれのページに対応するHTMLを作成します。
#### auth/login.blade.php
```
# mkdir -p resources/views/auth
# vim resources/views/auth/login.blade.php
```
```html:auth/login.blade.php
<form method="post" action="{{ route('login') }}">
    @csrf
    <label>
        ID:
        <input type="text" name="id" required>
    </label>
    <label>
        Password:
        <input type="password" name="password" required>
    </label>
    <button type="submit">Login</button>
</form>
```
#### dashboard.blade.php
```
# vim resources/views/dashboard.blade.php
```
```html:resources/views/dashboard.blade.php
<h1>Welcome to Dashboard</h1>
<a href="{{ route('logout') }}">Logout</a>
```
### 対応コントローラーの作成
#### AuthController...ログイン画面の表示とログイン処理
ログイン画面の表示と、ログイン・ログアウト処理に関わるメソッドを作成します。
```
# php artisan make:controller AuthController
# vim app/Http/Controllers/AuthController.php 
```
```php:app/Http/Controllers/AuthController.php 
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class AuthController extends Controller
{
        public function showLoginForm()
        {
                return view('auth.login');
        }

        public function login(Request $request)
        {
                $credentials = $request->only('id', 'password');

                if (Auth::guard('web')->attempt($credentials)) {
                        // デフォルトのリダイレクト先は/dashboard
                        return redirect()->route('dashboard');
                }

                return back()->withErrors(['id' => '認証に失敗しました。']);
        }


        public function logout()
        {
                Auth::logout();
                return redirect('/login');
        }
}
```
#### DashboardContloller
ログイン後の画面のコントローラーを開くための作成
```
# php artisan make:controller DashboardController
# vim app/Http/Controllers/DashboardController.php 
```
```php:app/Http/Controllers/DashboardController.php 
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class DashboardController extends Controller
{
    public function index()
    {
        return view('dashboard');
    }
}
```
### Webルーティングの設定
作成したWebページを開けるようにルーティングを設定します。
また、dashboardに関してはmiddleware('auth')を紐づけておくことで認証が必要なページとなります。
```
# vim routes/web.php
```
#### routes/web.php
```php:routes/web.php
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\DashboardController;

Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard')->middleware('auth');
Route::post('/login', [AuthController::class, 'login']);
Route::get('/logout', [AuthController::class, 'logout'])->name('logout');
```
## LDAP認証周りの仕組み作成
### LDAP認証ユーザー用のユーザーモデルを作成
下記コマンドでモデルを作成します。
```
# php artisan make:model Models/LdapUser
# vim app/Models/LdapUser.php
```
#### app/Models/LdapUser.php
Authenticatableクラスをimplementsする関係上、これらの関数は使う使わないに関わらず作成しておいてください。作成しておかないとエラーが出ます。
```php:app/Models/LdapUser.php
<?php
namespace App\Models;

use Illuminate\Contracts\Auth\Authenticatable;

class LdapUser implements Authenticatable
{
    private $username;

    public function __construct($username)
    {
        $this->username = $username;
    }

    public function getAuthIdentifierName()
    {
        return 'username';
    }

    public function getAuthIdentifier()
    {
        return $this->username;
    }

    public function getAuthPassword()
    {
        return '';
    }

    public function getRememberToken()
    {
        return null; 
    }

    public function setRememberToken($value)
    {   
    }

    public function getRememberTokenName()
    {
        return null; 
    }
}
```
### LDAP認証用のプロバイダーを作成
下記コマンドでLDAP認証用のプロパイダーを独自に作成します。
```
vim app/Providers/LdapUserProvider.php
```
#### app/Providers/LdapUserProvider.php
内容を記載します。特にretrieveByIdを正しく実装するようにしてください。
ここを実装しないと、認証済チェックがうまく動かず、ログインができません。

:::note
retrieveByIdメソッドが呼び出されることにより、Auth:check()等の認証済チェックが行われています。これがnullだと認証されていない扱いになります。
:::

```php:app/Providers/LdapUserProvider.php
<?php
namespace App\Providers;

use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;

class LdapUserProvider implements UserProvider
{
        public function retrieveById($identifier)
        {
                // キーに対応するユーザーモデルを返す
                return new \App\Models\LdapUser($identifier);
        }

        public function retrieveByToken($identifier, $token)
        {
        }

        public function updateRememberToken(Authenticatable $user, $token)
        {

        }

        public function retrieveByCredentials(array $credentials)
        {
                // LDAP接続とバインドの試行。
                $ldapconn = ldap_connect(env('LDAP_HOST'), env('LDAP_PORT'));
                if ($ldapconn) {
                        ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
                        $ldapbind = @ldap_bind(LDAPバインドの処理を書く);
                        if ($ldapbind) {
                                return new \App\Models\LdapUser($credentials['id']);
                        }
                }
                return null;
        }

        public function validateCredentials(Authenticatable $user, array $credentials)
        {
                return $user !== null;
        }
}
```
### 独自プロパイダ－を認識するように設定
独自に作ったプロパイダーをLaravelに認識させるように処理を書く必要がある。
#### app/Providers/AuthServiceProvider.php
boot()関数に、独自のプロパイダを登録する。今回を**「ldap-provider」**という名前で登録しています。
```php:app/Providers/AuthServiceProvider.php
// これを追加
use Illuminate\Support\Facades\Auth;
...
...

    public function boot()
    {
            // ここから
            Auth::provider('ldap-provider', function($app, array $config) {
                    return $app->make(LdapUserProvider::class);
                    return new LdapUserProvider();
            });
            // ここまで

            $this->registerPolicies();
    }

```
### 認証設定ファイルを更新
最後に認証にLDAP認証用プロパイダを使うように設定します。
```
vim config/auth.php
```
#### config/auth.php
Guardsのproviderに「ldap-provider」を設定します。これは任意の名称でいいです。
Providersにそれぞれ対応するプロパイダ－名(driver)とモデルを設定します。
```php:auth.php
    'guards' => [
            'web' => [
                'driver' => 'session',
                'provider' => 'ldap',
        ],
    ],

    'providers' => [
            'ldap' => [
                'driver' => 'ldap-provider',
                'model' => App\Models\LdapUser::class,
            ],
    ],
```

### 環境変数の設定
LDAPサーバーへの接続関連の環境変数を追加します。
#### .env
```
# vim .env
```
```
下記それぞれを環境に合わせて設定
LDAP_HOST=""
LDAP_PORT=
LDAP_BIND_DN=""
LDAP_BIND_PASSWORD=""
LDAP_BASE_DN=""
```

# 試す
- http://servicename.com/login → ログイン画面が開くことを確認
- ログイン後、http://servicename.com/dashboard に遷移できることを確認
- 別タブでhttp://servicename.com/dashboard にアクセスして認証済み扱いで開けることを確認
- ログアウト処理をして、ログイン画面に戻ることを確認
- ログアウト状態でhttp://servicename.com/dashboard にアクセスしたらログイン画面に戻されることを確認

# おわり
Laravel、わかってしまえばすごく使いやすいけど、わかるまでが大変な印象･･･
あとはデータベースから情報引いたりとかも試してみましょうかね。