2021-10-13
【Sign in with Slack】Laravel で Slack の OpenID Connect (OAuth 2.0) による認証を行う
公式ドキュメント: https://api.slack.com/authentication/sign-in-with-slack
認証処理の流れ
- ユーザが Laravel アプリケーションのログイン画面で「Sign in with Slack」ボタンを押下し、Slack の Authorizaion Endpoint へリダイレクトする。このとき、リダイレクトする URL には
redirect_uri
,state
,nonce
など OAuth および OpenID Connect で必要となるパラメータを付与する。 - ユーザはリダイレクト先で Slack にログイン (認証) およびユーザ情報提供の許可 (認可) を行う。
- Slack での認証・認可が完了すると、1. で指定した
redirect_uri
(コールバックページ) に一時承認コードが付与された状態で遷移する。 - コールバックページ では
state
パラメータの検証を行い、OK であれば一時承認コードを利用して Slack へid_token
をリクエストする。 id_token
受領後、nonce
パラメータの検証を行い、OK であればid_token
に含まれる Email アドレスと一致する登録済のユーザを取得する。- ユーザが取得できた場合、そのユーザとしてログインして、セッションを再生成した上でホーム画面 (
/home
) に遷移する。
+--------+ +--------+
| | | |
| |---------(1) AuthN Request-------->| |
| | | |
| | +--------+ | |
| | | | | |
| | | End- |<--(2) AuthN & AuthZ-->| |
| | | User | | |
| RP | | | | OP |
| | +--------+ | |
| | | |
| |<--------(3) AuthN Response--------| |
| | | |
| |---------(4) UserInfo Request----->| |
| | | |
| |<--------(5) UserInfo Response-----| |
| | | |
+--------+ +--------+
上図は OpenID Connect Core 1.0 - 1.3. Overview より引用
- AuthN: AutheNtication / 認証
- AuthZ: AuthoriZation / 認可
- RP: Relying Party, ここでは Laravel アプリケーション
- OP: OpenID Provider, ここでは Slack
Slack の OpenID Connect 利用にあたっての前提事項
クライアントアプリケーション (Laravel) には HTTPS かつ FQDN で接続可能である必要があります。
- Slack の OAuth では Redirect URI (コールバックページ) のサーバ証明書検証は行われないため、自己署名証明書(オレオレ証明書)で構いません。
- 以下のような URL は OK です。
https://example.com
https://dev.example.jp
- 以下のような URL は NG です。
http://example.com
-> HTTPS である必要がありますhttps://127.0.0.1
-> FQDN である必要がありますhttps://localhost
-> FQDN である必要があります
なお、Laravel Sail での接続の HTTPS 化については 過去記事 もご参照ください。
本記事で例示する Laravel アプリケーションの前提事項
- ホスト URL を
https://example.com
とします。 - OpenID Connect はログイン時のユーザ認証にのみ利用し、セッション管理は Laravel デフォルトの認証システムで行います。
- ユーザはサインインに利用する Slack ワークスペースと Laravel アプリケーション 双方に登録済であり、かつメールアドレスが同一であるものとします。
- HTTP Client には Guzzle を利用します。(https://docs.guzzlephp.org/en/stable/index.html)
- 利用バージョンは Laravel 8.0, Guzzle 7.0 です。
以下、手順です。
Slack App の作成と環境変数の設定
https://api.slack.com/apps から OAuth 用のアプリケーションを作成します。
作成した App の Client ID
と Client Secret
を .env
に記載しておきます。Client Secret
は名前の通り秘匿情報ですので、決してソースコードに直書きをしないでください。
※画像の ID 等はセキュリティの関係上、加工しています
.env
SLACK_CLIENT_ID="9999999999999.9999999999999"
SLACK_CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
なお、本記事の Laravel アプリケーションでは config
配下の const.php
で定数管理を行っているため、そちらにも追記します。
config/const.php
<?php
return [
'SLACK_CLIENT_ID' => env('SLACK_CLIENT_ID'),
'SLACK_CLIENT_SECRET' => env('SLACK_CLIENT_SECRET'),
// config('const.SLACK_CLIENT_SECRET') で値を取得できます
];
Slack App の OAuth & Permissions 設定
Slack App の設定画面に戻り、「OAuth & Permissions > Redirect URLs」にコールバックページの URL (https://example.com/slack/callback
) を追加します。
つづいて「OAuth & Permissions > Scopes > User Token Scopes」で OAuth のスコープを指定します。
Slack では OpenID 用のスコープとして openid
, email
, profile
が指定可能で、今回はメールアドレスでユーザ認証を行うため、 openid
と email
の2つを指定します。
これで Slack 側の設定は完了です。続いて Laravel アプリケーションの実装を行っていきます。
エンドポイントの追加
リダイレクト用ルート、コールバック用ルートをそれぞれ追加します。
routes/web.php
<?php
Route::get('/slack/redirect', 'OAuthController@redirect');
Route::get('/slack/callback', 'OAuthController@callback');
Controller の作成・記述
上述した OAuthController
を作成します。
php artisan make:controller OAuthController
app/Http/Controllers/OAuthController
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use App\User;
class OAuthController extends Controller
{
// 省略
}
以下、Controller の実装です。
コンストラクタ
OAuth で利用するパラメータをフィールドとして定義しておきます。
public function __construct()
{
// ログイン用ルートであるため、guest ミドルウェアをかまします
$this->middleware('guest');
$this->client_id = config('const.SLACK_CLIENT_ID');
$this->client_secret = config('const.SLACK_CLIENT_SECRET');
$this->redirect_uri = 'https://example.com/slack/callback';
}
redirect メソッド
ログイン画面で「Sign in with Slack」ボタンを押下時、このエンドポイントに遷移させ、 Slack の Authorization Endpoint へリダイレクトを行います。セキュリティ上のポイントは次の2点です。
state
にcsrf_token()
を利用すること- CSRF 対策として、リダイレクトとコールバックが同一のセッションであることを検証するために設定します。
nonce
は UUID をセッションに格納しておくこと- リプレイアタック対策として、セッションと
id_token
を紐付けるために設定します。 - Laravel ではセッションヘルパの
put()
メソッドを利用します。
- リプレイアタック対策として、セッションと
public function redirect()
{
$state = csrf_token();
$nonce = uniqid();
request()->session()->put('nonce', $nonce);
$to = "https://slack.com/openid/connect/authorize" .
"?response_type=code" .
"&scope=openid,email" .
"&state={$state}" .
"&nonce={$nonce}" .
"&client_id={$this->client_id}" .
"&redirect_uri={$this->redirect_uri}";
return redirect($to);
}
callback メソッド
Slack での認証・認可が完了すると、このエンドポイントに戻ってきます。
※解説の都合上、メソッドの切り分けは行っていません。
public function callback()
{
// state の検証
if (csrf_token() !== request('state')) {
abort(401);
}
// id_token のリクエスト
$client = new \GuzzleHttp\Client();
$res = $client->request('POST', "https://slack.com/api/openid.connect.token", [
'form_params' => [
'client_id' => $this->client_id,
'client_secret' => $this->client_secret,
'code' => request('code'),
'redirect_uri' => $this->redirect_uri
]
]);
// レスポンスのステータスチェック
$status = $res->getStatusCode();
if ($status !== 200) {
abort(401);
}
$contents = json_decode($res->getBody()->getContents());
if (!$contents->ok) {
abort(401);
}
// JWT の payload の取得
$id_token = explode('.', $contents->id_token);
$payload = json_decode(base64_decode($id_token[1]));
// nonce の検証
$session_nonce = request()->session()->pull('nonce');
if ($session_nonce !== $payload->nonce) {
abort(401);
}
// ユーザの取得
$user = User::where('email', $payload->email)->firstOrFail();
// ログイン処理
Auth::login($user);
request()->session()->regenerate();
return redirect('/home');
}
以下、要点を解説します。
state
の検証- 前述の通り、リダイレクトとコールバックのセッションが同一であることを確認するため、リクエストに付与された
state
パラメータとセッションのcsrf_token()
が同一であることを検証します。
- 前述の通り、リダイレクトとコールバックのセッションが同一であることを確認するため、リクエストに付与された
id_token
のリクエスト- パラメータの一時承認コード (
code
) を利用して、Slack にid_token
をリクエストします。
- パラメータの一時承認コード (
JWT の payload の取得
- Slack からは JWT 形式で
id_token
が返却されるため、そこから payload を抽出します。 - ドットを区切り文字として分割し、2つ目の文字列 (= payload) を base64 デコードします。
- Slack からは JWT 形式で
nonce
の検証- リダイレクト時にセッションに格納しておいた
nonce
と、JWT の payload に含まれるnonce
が一致することを確認します。 - このとき、リプレイアタックを防ぐ目的からセッションに格納された
nonce
は削除する必要があります。Laravel では セッションヘルパのpull()
メソッドを利用することで、値の取得と削除を同時に行うことができます。
- リダイレクト時にセッションに格納しておいた
ユーザの取得
- JWT の payload に含まれるメールアドレスを利用してユーザを取得します。
- サンプルコードでは
firstOrFail()
を使用していますが、ユーザが取得できなかった場合には未登録である旨をユーザに知らせるなど、適宜ハンドリングを行ってください。
ログイン処理
- 取得できたユーザとしてログインさせたのち、セッションを再生成します。
ログイン画面に「Sign in with Slack」ボタンを配置
最後にボタンを配置して完了です。
Slack 公式でボタンジェネレータも用意されています。※ボタンのリンク先を /slack/redirect
に置き換えて配置してください。
https://api.slack.com/authentication/sign-in-with-slack#generator
<a href="/slack/redirect" style="align-items:center;color:#000;background-color:#fff;border:1px solid #ddd;border-radius:4px;display:inline-flex;font-family:Lato, sans-serif;font-size:16px;font-weight:600;height:48px;justify-content:center;text-decoration:none;width:256px"><svg xmlns="http://www.w3.org/2000/svg" style="height:20px;width:20px;margin-right:12px" viewBox="0 0 122.8 122.8"><path d="M25.8 77.6c0 7.1-5.8 12.9-12.9 12.9S0 84.7 0 77.6s5.8-12.9 12.9-12.9h12.9v12.9zm6.5 0c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V77.6z" fill="#e01e5a" /><path d="M45.2 25.8c-7.1 0-12.9-5.8-12.9-12.9S38.1 0 45.2 0s12.9 5.8 12.9 12.9v12.9H45.2zm0 6.5c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H12.9C5.8 58.1 0 52.3 0 45.2s5.8-12.9 12.9-12.9h32.3z" fill="#36c5f0" /><path d="M97 45.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9-5.8 12.9-12.9 12.9H97V45.2zm-6.5 0c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V12.9C64.7 5.8 70.5 0 77.6 0s12.9 5.8 12.9 12.9v32.3z" fill="#2eb67d" /><path d="M77.6 97c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9-12.9-5.8-12.9-12.9V97h12.9zm0-6.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H77.6z" fill="#ecb22e" /></svg>Sign in with Slack</a>
最後に
Slack の OpenID Connect による認証の実装は以上となります。
OAuth や OpenID Connect はソーシャルログインが手軽に実装できる一方で、クライアント側の実装を適切に行わないと脆弱性が顕在化し、セキュリティインシデントの原因となります。OAuth および OpenID Connect の仕様を理解した上で、正しく実装することでセキュアなアプリケーション開発を行いましょう。(自戒を込めて。)
参考
- https://api.slack.com/authentication/sign-in-with-slack
- https://api.slack.com/methods/openid.connect.token
- http://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html
- http://openid-foundation-japan.github.io/rfc6749.ja.html
- https://docs.guzzlephp.org/en/stable/index.html
- https://laravel.com/docs/8.x/session