在 Laravel 中使用 Kratos 認證

Laravel 有著優秀的預定義認證(Authentication)功能,讓開發者不必費心在重複製作用戶註冊、登入、登出等功能。

無論是早期的 laravel/ui 還是 laravel/fortify 都提供了安全、完整且方便的解決方案。

Kratos 是由 Ory Corp 所提供的開源認證解決方案,藉由設定檔的方式可以靈活設計認證模型(例如帳號密碼、第三方社群或 WebAuth 等 passwordless 的形式)

使用方式

為了在 Laravel 中更好使用 Kratos,我寫了一個 Laravel Package:chivincent/laravel-kratos

可以在 chivincent/laravel-kratos-demo 中找到完整版的程式碼

建立 Laravel Application

詳細可以在 Installation 中找到如何建立新的 Laravel 應用程式

$ laravel new laravel-kratos-demo

出於習慣,在開發純 API 的專案時,我會習慣性移除一些檔案

安裝套件

$ composer require chivincent/laravel-kratos
$ php artisan vendor:publish --provider="Chivincent\LaravelKratos\KratosServiceProvider"

在通常情況下,config/kratos.php 保持預設即可


config/auth.php 加入以下內容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
return [
    // ...
    'guards' => [
        'web' => [
            // ...
        ],

        'kratos' => [
            'driver' => 'kratos',
            'provider' => 'kratos', // or 'kratos-database'
        ],
    ],
    // ...
];

其中 kratoskratos-database 分別是對應不同的 UserProvider:

  • kratos 會使用 Kratos API 的 Identity
  • kratos-database 是使用 Eloquent ORM 直接以 ID 存取 Kratos 服務所使用的資料庫取得 Model

兩者沒有優劣,但若使用 kratos-database 需要在正式環境中設定好權限。


config/cors.php 更改以下內容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

return [
    // ...
    
    'allowed_origins' => ['http://127.0.0.1:4455'],
    
    // ...
    
    'supports_credentials' => true,
    
    // ...
]; 

此處的 allowed_origins 使用 :4455 是因為 Kratos UI 會建立在 Port 4455 上,如果有自己的 UI 請自行設定合適的 Port。

註:此處無法使用 'allowed_origins' => '*'

註2:Kratos Cookie 將會使用 127.0.0.1 而不是 localhost,請務必注意要用 127.0.0.1 存取 API

Database 連線

如果使用 kratos-databse 作為 provider,需要額外設定資料庫連線

config/database.php 中的 connection 加入以下內容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
return [
    // ...
    'connections' => [
        'kratos' => [ // connection name should as same as `config('kratos.user_providers.kratos-database.connection')` 
            'driver' => 'pgsql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '5432'),
            'database' => env('DB_KRATOS_DATABASE', 'kratos'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8',
            'prefix' => '',
            'prefix_indexes' => true,
            'search_path' => 'public',
            'sslmode' => 'prefer',
        ],    
    ]
    // ... 
];

此處使用的是 PostgreSQL,如果是 MySQL 的用戶請自行更改設定

以 Docker 啟動服務

建立 docker/services/database/0-init-kratos-db.sh

1
2
3
4
5
6
7
#!/bin/bash
set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
    CREATE DATABASE kratos;
	GRANT ALL PRIVILEGES ON DATABASE kratos TO $POSTGRES_USER;
EOSQL

這是為了額外建立 Database 給 Kratos 服務

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# docker-compose.yaml
networks:
  kratos-demo:

services:
  database:
    image: 'postgres:alpine'
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_USER=forge
      - POSTGRES_PASSWORD=password
      - PGPASSWORD=password
      - POSTGRES_DB=forge
    volumes:
      - type: bind
        source: ./docker/services/database
        target: /docker-entrypoint-initdb.d
    networks: ['kratos-demo']

設定 Database 為 PostgreSQL


建立 docker/services/kratos,並且將 https://github.com/ory/kratos/tree/master/contrib/quickstart/kratos/email-password 的兩個檔案複製進來。

註:其實可以試著更換成其它不同的設定檔,Kratos UI 上會有不同的變化

建立 docker/compose/kratos-svc.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
services:
  kratos-migrate:
    depends_on: [ 'database' ]
    image: 'oryd/kratos:v0.10.1'
    environment:
      - DSN=postgres://forge:[email protected]:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
    volumes:
      - type: bind
        source: ./docker/services/kratos
        target: /etc/config/kratos
    command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes
    restart: on-failure
    networks: ['kratos-demo']

  kratos-selfservice-ui-node:
    image: oryd/kratos-selfservice-ui-node:v0.10.1
    ports:
      - '4455:4455'
    environment:
      - PORT=4455
      - SECURITY_MODE=
      - KRATOS_PUBLIC_URL=http://kratos:4433/
      - KRATOS_BROWSER_URL=http://127.0.0.1:4433/
    restart: on-failure
    networks: [ 'kratos-demo' ]

  kratos:
    depends_on: [ 'kratos-migrate' ]
    image: 'oryd/kratos:v0.10.1'
    ports:
      - '4433:4433' # public
      - '4434:4434' # admin
    environment:
      - DSN=postgres://forge:[email protected]:5432/kratos?sslmode=disable&max_conns=20&max_idle_conns=4
      - LOG_LEVEL=trace
    volumes:
      - type: bind
        source: ./docker/services/kratos
        target: /etc/config/kratos
    command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier
    restart: unless-stopped
    networks: ['kratos-demo']

  mailslurper:
    image: 'oryd/mailslurper:latest-smtps'
    networks: ['kratos-demo']
    ports:
      - '4436:4436'
      - '4437:4437'

這個設定改寫自官方的 quickstart.ymlquickstart-standalone.ymlquickstart-postgres.yml


最後,即可利用 docker compose 啟動這些服務

1
$ docker compose -f docker-compose.yaml -f docker/compose/kratos-svc.yaml up -d

設計 Laravel API

此時,我們可以實際設計 Laravel API

routes/api.php 更改為以下內容

1
2
3
4
5
6
7
<?php

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

Route::middleware('auth:kratos')
    ->get('/user', fn (Request $request) => response()->json($request->user()));

使用服務

  • 以瀏覽器打開 http://127.0.0.1:4455,這是官方提供的 Kratos UI,可以註冊、登入、登出、修改用戶資料跟 Email 認證
  • 利用 Fetch API 實際操作 Laravel API
1
2
3
4
5
6
7
const headers = new Headers({
    'accept': 'application/json',
    'content-type': 'application/json',
})

resp = await fetch('http://127.0.0.1:8000/api/user', { headers, credential: 'include' })
await resp.json()

關於 Kratos 的認證細節

Kratos 採用 Session Cookie 進行認證,其 cookie 名為 ory_kratos_session

註:Kratos 不使用 OpenID Connect 或 JWT 進行認證,這是因為有其它的專案專門執行這些工作(HydraOathKeeper

一般而言,應用程式只需要負責把 cookie string 打到 http://{kratos}/sessions/whoami 就可以確認用戶的 Session 是否合法,詳情可查閱文件

Kratos Session 的格式是一組 base64 encoded 字串,其實現來自於 gorilla/session,而底層為 gorilla/securecookie

ory_kratos_session 解碼後,可以看到它由三個部份組成:

  • date:核發時間
  • value:實際值
  • mac:HMAC 校驗碼

註:解碼時需要注意,它是使用 golang 的 base64.URLEncoding.EncodeToString(),它會將 + 替換為 -

預設上會使用 HMAC-SHA256 將 ory_kratos_session|{date}|{value}kratos.yaml 中的 secrets.cookie 生成校驗碼

以下用 PHP 實現校驗流程(以下僅作為示範,出於安全性考量,非常不建議自行實作):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$cookie = 'MTY2ODY1OTI0M3w5UTkzSVpud2ZqZi11SlI4aDVkMG1PRGxQTWFuMjdmanNfdUJsZUtNRnRJOGIxSDNSR2tKbG5NYUllNXFoUDQ3bVQ4ZGNFWjdpNFctdWZoTXJwTjJVTGMtdi1TU0l0cHhSdWZ0dFZMLWFIaDVZSzBhQ1d3cFNIbWJzUEltR01kZU9OTlk2NmlWbGc9PXwonlgqIrSfRnDjbD6RbThrSZky2c-2MFkU2Q6V3E2f3w==';

$decoded = base64_decode(str_replace('-', '+', $cookie), true);

[$date, $value, $mac] = $decoded;

$result = hash_equals(
    $mac,
    hash_hmac('sha256', "ory_kratos_session|$date|$value", $key ?: 'PLEASE-CHANGE-ME-I-AM-VERY-INSECURE', true),
);

if (! $result) {
    throw new Exception('Invalid');
}