各種情報
前提条件
- 言語はPHPで、バージョンは 5.6 を想定
- フレームワークに CodeIgniter 3 を使用
- WebサイトのURLは、https://example.com と想定して説明します
参考URL
- Yahoo!JAPANデベロッパーネットワーク Yahoo!ID連携v2 Authorization Code フロー https://developer.yahoo.co.jp/yconnect/v2/authorization_code/
- CodeIgniterユーザガイド http://codeigniter.jp/user_guide/3/index.html
多用している CodeIgniter のコマンド
redirect(リダイレクト先, リダイレクトメソッド, HTTPレスポンスコード)
リダイレクト先へリダイレクトします。リダイレクト先に ‘login’ と指定していれば、Loginコントローラーのindexメソッドへリダイレクトします(http://example.com/login/index)。素のPHPであれば、header(‘Location: http://example.com/login/index’); のようになります。
$this->input->get_post('・・・') $this->input->post('・・・')
$_GET
や$_POST
から値を取り出します。その値(項目)がないときは、null
を返してくれます。get_post()
は先に $_GET
を調べて、値がなければ $_POST
を調べてくれます。post()
は $_POST
だけを調べます。
$this->load->library('・・・') $this->load->view('・・・')
ライブラリやビューとして作成した PHP ファイルをインクルードします。require_once '・・・'
と同じ意味です。
random_string('md5')
ランダムな文字列を作成する関数です。'md5'
を指定しているのはちょうど 32 文字の文字列を返してくれるからで、他意はありません。推測されにくいランダムな英数字の文字列であることが重要です。
Yahooログイン専用ライブラリの作成
Yahooログイン処理用のライブラリを作成します。CodeIgniterでは下記コード例のように、クラスとして作成します。
ログイン処理に必要な ClientID とシークレットは、あらかじめ変数などに入れておきます。
Yahoo_library.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Yahooライブラリ
*/
class Yahoo_library
{
/**
* フィールド
*/
private $ci;
// アプリ情報
// ClientIDは末尾のハイフンも忘れずに(ハイフンがある場合)
private $clientId = '00000000000000000000000000000000000000000000';
private $yahooSecret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
// リダイレクトURL
private $redirectUri = 'https://example.com/login/yahoo';
// エンドポイント
private $openidConfigurationEndPoint = 'https://auth.login.yahoo.co.jp/yconnect/v2/.well-known/openid-configuration';
private $publickeyEndPoint = 'https://auth.login.yahoo.co.jp/yconnect/v2/public-keys';
/**
* コンストラクタ
*/
public function __construct()
{
$this->ci = &get_instance();
// ライブラリをロードしておく
$this->ci->load->library('jwt');
$this->ci->load->library('curl_library');
}
}
Loginコントローラーの準備
Yahooログイン用ライブラリをあらかじめロードしておくように、コンストラクターにコマンドを追加します。
Login.php
public function __construct()
{
parent::__construct();
$this->load->library('line_library');
$this->load->library('facebook_library');
$this->load->library('instagram_library');
$this->load->library('google_library');
// 以下の行を追加
$this->load->library('yahoo_library');
}
ログインページで「Yahooでログイン」をクリックしたとき、ログイン処理が開始されるよう、index メソッドを以下のように修正します。
Login.php
public function index()
{
// 押されたボタンによってログインの処理を分岐
switch ($this->input->post('submit')) {
case 'line':
redirect('login/login_line', 'location', 302);
break;
case 'yahoo':
// 以下の行を追加する
redirect('login/login_yahoo', 'location', 302);
break;
case 'twitter':
break;
case 'instagram':
redirect('login/login_instagram', 'location', 302);
break;
case 'facebook':
redirect('login/login_facebook', 'location', 302);
break;
case 'google':
redirect('login/login_google', 'location', 302);
break;
}
認証エンドポイントへリダイレクトする(login_yahooメソッドの作成)
このメソッドがログインの第一歩です。
最初に行うことは、OpenID Configurationエンドポイントにリクエストして、ログインに必要な各エンドポイントの URL を取得することです。
取得したエンドポイントのうち Authorization エンドポイントへリクエストすると、Yahoo の認証画面(ログイン画面)がユーザーのブラウザーに表示されます。ユーザーが Yahoo にログインすると、登録済みのリダイレクト URL へリダイレクトします(または、そのURLが返ってくる)。
Login.php
/**
* Yahooによるログインを開始する
*/
public function login_yahoo()
{
// エンドポイントを取得し、セッションに保管する
$endpoint = $this->yahoo_library->getEndPoint();
$_SESSION['endpoint'] = $endpoint;
// 認証コードのリクエスト
$url = $this->yahoo_library->requestAuthorization($endpoint['authorization']);
if (!empty($url)) {
header('Location: ' . $url);
exit;
}
}
エンドポイントの取得は Yahoo_library ライブラリの getEndPoint メソッドで行います。このメソッドは後ほど作成します。
取得したエンドポイントは後で使用するので、セッションに保管しておきます。
取得したエンドポイントのうち authorization エンドポイントへリクエストすると、ユーザーのブラウザーに認証画面が表示されます。
エンドポイントのリクエスト(getEndPointメソッドの作成)
ログインに必要なエンドポイントの URL を取得するには、OpenID Configuration エンドポイントへリクエストします。この処理を Yahoo_library ライブラリの getEndPoint メソッドで行います。
Yahoo_library.php
/**
* 認証のための各エンドポイントを取得する
* @return array エンドポイントの配列
*/
public function getEndPoint()
{
$value = $this->ci->curl_library->execGetCurl($this->openidConfigurationEndPoint, null);
$result = [
'authorization' => isset($value['authorization_endpoint']) ? $value['authorization_endpoint'] : '',
'token' => isset($value['token_endpoint']) ? $value['token_endpoint'] : '',
'userinfo' => isset($value['userinfo_endpoint']) ? $value['userinfo_endpoint'] : '',
'jwks' => isset($value['jwks_uri']) ? $value['jwks_uri'] : '',
'issuer' => isset($value['issuer']) ? $value['issuer'] : '',
];
return $result;
}
OpenID Configurationエンドポイントへ GET でリクエストすると、各エンドポイントの URL が配列(実際はJSON)で返ってきます。レスポンスにはエンドポイントの URL 以外に多くの情報が含まれているので、必要なものだけ抜き出して、メソッドの戻り値としています。
Authorizationエンドポイントへリクエストする(requestAuthorizationメソッドの作成)
Authorizationエンドポイントへ GET、または POST でリクエストします。ユーザーのブラウザーに Yahoo の認証画面(ログイン画面)が表示されます。ユーザーがログインするとあらかじめ登録していたリダイレクト URL へリダイレクトし(または、リダイレクトURLを返してくる)、認可コードを取得できます。
下記のコード例では POST でリクエストしています。
Yahoo_library.php
/**
* 認証コードをリクエストする
* @param string $endpoint 認証エンドポイント
*/
public function requestAuthorization($endpoint)
{
$this->ci->load->helper('string');
// 検証用にランダムな文字列を作成し、セッションに保管する
$state = random_string('alnum', 32);
$nonce = random_string('alnum', 32);
$auth = [
'state' => $state,
'nonce' => $nonce,
];
$_SESSION['auth'] = $auth;
// 送信パラメーター
$params = [
'response_type' => 'code',
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
'bail' => 1,
'scope' => 'openid profile email address',
'state' => $state,
'nonce' => $nonce,
'display' => 'auto',
'max_age' => 3600,
];
// リクエスト実行
$response = $this->ci->curl_library->execPostCurl($endpoint, $params, null, ['redirect_url']);
$redirectUrl = isset($response['redirect_url']) ? $response['redirect_url'] : null;
return $redirectUrl;
}
まず、ランダムな文字列を2つ作成します。1つは state、もう1つは nonce となります。state は CSRF 対策の文字列です。nonce はリプレイアタック対策の文字列です。
作成した state と nonce は後で検証するので、一旦セッションに保管しておきます。
上記のコードで Authorization エンドポイントへ送信しているパラメーターは、以下のとおりです。
パラメーター | 内容 |
---|---|
response_type | 'code' を指定します。 |
client_id | Client IDを指定します。 |
redirect_uri | あらかじめ登録したリダイレクトURL(の1つ)を指定します。ユーザーがYahooにログイン後、このURLにリダイレクトされます(または、リダイレクト先として返してくる)。 |
bail | '1' を指定すると、ユーザーがYahooへのログインをキャンセルした場合でも、リダイレクトURLへリダイレクトします。 このパラメーターを指定しなかった場合、Yahooのトップページへリダイレクトします。 |
scope | アプリの権限を指定します。複数の権限を指定する場合、半角空白で区切ります。このコード例では 'openid'、'profile'、'email'、'address' の4つのスコープを指定しています。 |
state | CSRF 対策の文字列です。 |
nonce | リプレイアタック対策の文字列です。 |
display | ログイン画面の表示方法を指定します。 |
max_age | 認証の有効秒数を指定します。 |
なお、上記に示した以外にも指定できるパラメーターがあります。すべてのパラメーターを知るにはYahooのドキュメントを参照してください。
Authorization エンドポイントへのリクエストは GET、または POST で行います。上記のコード例では POST を使用していますが、おそらく通常は、エンドポイントにパラメーターを付けてリダイレクトする方法が一般的と思います。
POST を使用すると、curl 実行結果の情報として redirect_url が返ってきます(CURLOPT_FOLLOWLOCATION が false の場合に限る)。redirect_url を取得できた場合、この URL へリダイレクトさせる必要があります。
なお、Curlのオプションに CURLOPT_FOLLOWLOCATION を true で指定すれば、勝手にリダイレクトするのでそちらの方がよいかもしれません。
コールバックの処理(yahooメソッドの作成)
ここで説明している例では、ユーザーが Yahoo にログインした後、Login コントローラーの yahoo メソッドへリダイレクトする設定になっています。そのため、yahoo メソッドを作成します。
なお、yahooメソッドで行う処理が多いため、分割しながら少しずつ説明します。
認可コードを取得する
ユーザーがYahooにログインし、アプリが求める権限を承認した場合、リダイレクトURLにパラメーターとして認可コードが付いてきます。認可コードは code パラメーターで示されます。
パラメーターに認可コードが付いていない場合、ユーザーがキャンセルしたか、またはその他のエラーが発生したことを示します。ユーザーがキャンセルした場合、error パラメーターが付いています。
もし、code も error も付いていない場合、想定外のエラーとします。
Login.php
/**
* Yahooログインからのコールバックを処理する
*/
public function yahoo()
{
// 認可コードの確認
$code = $this->input->get_post('code');
if (empty($code)) {
// 認可コードがない場合、エラーを取得
$error = $this->input->get_post('error');
if (empty($error)) {
// エラーが含まれないときは不明なエラー
$_SESSION['message'] = 'エラーが発生しました。はじめからやり直すか、別のログイン方法を試してください。';
} else {
// エラーが含まれるときはログインキャンセルとみなす
$_SESSION['message'] = 'ログインがキャンセルされました。';
}
redirect('login', 'location', 302);
}
上記では、error が含まれるときはログインキャンセルとみなしていますが、実際は送信したパラメーターに誤りがあったなど、その他のエラーも含まれます。公開するWebサイトに実装する場合には注意してください。
stateを確認する
認可コードを取得できたら、state を確認します。Yahoo のエンドポイントへ送信した state が、そのまま state パラメーターとして戻ってきています。戻ってきたstateパラメーターの文字列と、セッションに保管している state の文字列が一致していなければなりません。
Login.php
public function yahoo()
{
// 認証コードの確認
...省略...
// パラメーターからstateの取得
$state = $this->input->get_post('state');
// セッションのstateを読み取る
if (!isset($_SESSION['auth'])) {
$_SESSION['message'] = '認証できません。';
redirect('login', 'location', 302);
}
$auth = $_SESSION['auth'];
unset($_SESSION['auth']);
// stateが一致することを確認する
if ($auth['state'] !== $state) {
$_SESSION['message'] = '認証に失敗しました。';
redirect('login', 'location', 302);
}
セッションに保管していたstateを読み出したとき、セッションのデータは削除しておきます。セッションに保管したままの場合、不正なレスポンスが同じ state をそのまま使用しても、不正かどうかを判断できなくなります。1つの文字列を使用できるのは1回きりにしておく必要があります。
アクセストークンをリクエストする
認可コードを取得でき、state の一致を確認したら、アクセストークンをリクエストします。
アクセストークンのリクエストは、tokenエンドポイントに行います。最初に取得したエンドポイントをセッションから読み取りましょう。
token エンドポイントからのレスポンスにはアクセストークンのほかに、リフレッシュトークンや ID トークンなどを含む配列になります。取得したレスポンスが配列でない場合、エラーと判断します。
Login.php
public function yahoo()
{
// 認証コードの確認
...省略...
// stateが一致することを確認する
...省略...
// エンドポイントの一覧をセッションから読み込む
$endpoint = $_SESSION['endpoint'];
unset($_SESSION['endpoint']);
// tokenエンドポイントへアクセストークンのリクエスト
$token = $this->yahoo_library->requestToken($endpoint['token'], $code);
if (!is_array($token)) {
$_SESSION['message'] = 'アクセストークンを取得できませんでした。' . $token;
redirect('login', 'location', 302);
}
requestToken メソッドは次に作成します。
requestTokenメソッドの作成(Yahoo_libraryライブラリ)
アクセストークンをリクエストする処理は、Yahoo_library ライブラリの requestToken メソッドに実装します。
Yahoo_library.php
/**
* トークンをリクエストする
* @param string $endpoint トークン取得エンドポイント
* @param string $code 認証コード
* @return mixed 取得成功:トークンの配列 取得失敗:エラー情報
*/
public function requestToken($endpoint, $code)
{
// パラメーター作成
$params = [
'grant_type' => 'authorization_code',
'redirect_uri' => $this->redirectUri,
'code' => $code,
];
// curlに指定するオプション
$options = [
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $this->clientId . ':' . $this->yahooSecret,
];
// リクエスト
$response = $this->ci->curl_library->execPostCurl($endpoint, $params, $options);
// レスポンスが空ではないか
if (empty($response['response'])) {
return 'レスポンスコード: ' . $response['code'];
}
$_response = $response['response'];
// レスポンスにエラーが含まれていないか
if (isset($_response['error'])) {
return $_response['error'] . ':' . $_response['error_description'];
}
// アクセストークンが含まれているか
if (!isset($_response['access_token'])) {
return 'アクセストークンを取得できませんでした。';
}
return $_response;
}
tokenエンドポイントへは以下のパラメーターを送信します。
パラメーター | 内容 |
---|---|
grant_type | 'authorization_code' を指定します。 |
redirect_uri | 登録済みのリダイレクトURLを指定します。 |
code | 認可コードを指定します。 |
tokenエンドポイントには Basic 認証が必要です。このため、curl の CURLOPT_HTTPAUTH オプションに、CURLAUTH_BASIC を指定しなければなりません。
Basic 認証のユーザーID は Client ID、パスワードはシークレットになります。Client ID とシークレットを':'(コロン)
でつないだ文字列を、CURLOPT_USERPWD オプションに指定します。
tokenエンドポイントへのリクエストは POST で実行します。
アクセストークンの取得に失敗すると、レスポンスに error が含まれます。
error が含まれていなければ、取得したレスポンスがメソッドの戻り値になります。
IDトークンをデコードする
tokenエンドポイントからのレスポンスには、アクセストークンのほかにIDトークンが含まれます。IDトークンはJWTでエンコードされているので、これをデコードします。
IDトークンには検証用のデータが含まれます。ユーザー認証を行う際は、IDトークンに含まれる認証情報を元に、ユーザーセッション管理を行います。
public function yahoo()
{
// 認証コードの確認
...省略...
// stateが一致することを確認する
...省略...
// tokenエンドポイントへアクセストークンのリクエスト
...省略...
// IDトークンを復号化する
$payload = $this->yahoo_library->decodeIDToken($token['id_token'], $auth['nonce'], $endpoint['issuer'], $token['access_token'], $code);
if ($payload === false) {
$_SESSION['message'] = 'IDトークンのデコードに失敗しました。';
redirect('login', 'location', 302);
}
IDトークンのデコードと検証は、Yahoo_library ライブラリの decodeIDToken メソッドに実装します。
decodeIDTokenメソッドの作成(Yahoo_libraryライブラリ)
Yahoo_library.php
/**
* IDトークンをデコードする
* @param string $code エンコードされたIDトークン
* @param string $nonce nonce
* @param string $issuer IDトークン発行者
* @param string $accessToken アクセストークン
* @param string $validationCode 認可コード
* @return mixed 復号成功:配列 復号失敗:false
*/
public function decodeIDToken($code, $nonce, $issuer, $accessToken, $validationCode)
{
// -------------------------------------
// IDトークン(JWT)のヘッダーからキーIDを取得する
// IDトークンの仮デコード
$parts = $this->ci->jwt->getAll($code);
if (is_null($parts['header'])) {
return false;
}
// ヘッダーを配列に変換
$decodeHeader = json_decode($parts['header'], true);
if (!isset($decodeHeader['kid'])) {
return false;
}
// キーIDを取得
$keyId = $decodeHeader['kid'];
// -------------------------------------
// 署名検証に必要な公開鍵を取得する
// 公開鍵の一覧をリクエスト
$keys = $this->ci->curl_library->execGetCurl($this->publickeyEndPoint, null);
// 使用する公開鍵を選択
$encKey = null;
foreach ($keys as $kid => $publickey) {
if ($kid === $keyId) {
$encKey = $publickey;
}
}
if (is_null($encKey)) {
return false;
}
// -------------------------------------
// IDトークンをデコードする
// ペイロードの検証用データ
$appends = [
'iss' => $issuer,
'aud' => $this->clientId,
'nonce' => $nonce,
];
// デコードと署名の検証
$payload = $this->ci->jwt->decode($code, $encKey, $appends);
if (is_null($payload)) {
return false;
}
// アクセストークンのハッシュ検証
if (isset($payload['at_hash'])) {
if ($this->validateHash($accessToken, $payload['at_hash']) === false) {
return false;
}
}
// 認証コードのハッシュ検証
if (isset($payload['c_hash'])) {
if ($this->validateHash($validationCode, $payload['c_hash']) === false) {
return false;
}
}
// 有効期限の検証
if (intval($payload['iat']) < time() - 300) {
return false;
}
return $payload;
}
まずはIDトークンを仮デコードし、ヘッダー部を取得します。ヘッダー部には署名のアルゴリズムや、署名に使用した公開鍵の ID が含まれています。
IDトークンを仮でコードする getAll メソッドはまだ作成していません。これは後ほど作成します。
署名に使用する公開鍵は、Public Keys エンドポイントから取得できます。エンドポイントからは公開鍵の配列を取得できます。公開鍵ごとにIDが設定されています。IDトークンのヘッダーに含まれるキーIDが、使用されている公開鍵を示しています。
公開鍵を取得したら Dトークンを正式にデコードし、ペイロード部を取得します。
ペイロード部には iss、aud、nonce という項目が含まれています。それぞれトークンの発行者、Client ID、nonceとなります。
最初に OpenID Configuration エンドポイントから取得した issuer という項目が、IDトークンの発行者を示しています。この issuer と、ペイロードの iss が一致する必要があります。
ペイロードの aud は Client ID を示しています。ペイロードのaudとアプリのClient IDが一致していることを確認します。
ペイロードの nonce は、認可コードをリクエストする際に作成した nonce と一致します。これが一致しない場合、リプレイアタックの可能性があります。
署名、issuer、aud、及び nonce は、先に作成していた JWT ライブラリで検証されます。JWT ライブラリの decode メソッドが NULL ではない値を返してきたら、前述のデータは正しく検証できたことになります。
しかし、これ以外にも検証するデータがあります。
ペイロードには at_hash という項目があります。at_hash はアクセストークンのハッシュです。先に取得したアクセストークンからハッシュを作成し、at_hash と一致することを確認します。ハッシュを検証するために validateHash メソッドを後ほど作成します。
また、同様にペイロードには認可コードのハッシュが含まれます。こちらは c_hash という項目です。アクセストークンと同じ手順で認可コードのハッシュを作成し、c_hash と一致することを確認します。
最後に、トークンの有効期限を確認します。iat という項目が有効期限を示しています。
なお、現在時刻から 300 (秒)を引いているのは、ログインを開始してから、有効期限を検証するまでの経過時間を加味しています。これは実際にかかる時間の平均的な値を設定してください。
以上の検証で問題がなければ、トークンは信頼できると判断します。
IDトークンの仮デコード(getAllメソッドの作成)
以前作成した JWT ライブラリに、getAll メソッドを追加します。getAll メソッドは ID トークンのヘッダーとペイロードをデコードして返します。署名を検証しませんので、このメソッドで取得したペイロードは信頼できないデータとして扱ってください。
Jwt.php
/**
* デコード結果をすべて返す
* @param string $data デコードするデータ
* @return array すべてのデータ
*/
public function getAll($data)
{
$result = [
'header' => null,
'payload' => null,
'signature' => null,
];
$divs = $this->divideData($data);
if ($divs === false) {
return $result;
}
$decoded = [
'header' => isset($divs['header']) ? $this->decodeBase64Uri($divs['header']) : null,
'payload' => $this->decodeBase64Uri($divs['payload'],
'signature' => $this->decodeBase64Uri($divs['signature']),
];
$result['header'] = $decoded['header'];
$result['payload'] = $decoded['payload'];
$result['signature'] = base64_encode($decoded['signature']);
return $result;
}
戻り値は header、payload、signature を項目に持つ連想配列です。header と payload は JSON 形式の文字列で、signature は BASE64 エンコードした文字列になります。
アクセストークンと認可コードのハッシュを検証する(validateHashメソッドの作成)
アクセストークンと認可コードのハッシュを作成し、それぞれ検証します。validateHashメソッドは Yahoo_library ライブラリに追加します。
ハッシュ作成方法は、Googleログインでアクセストークンのハッシュを作成した手順と同じです。
Yahoo_library.php
/**
* ハッシュを検証する
* @param string $code 検証するコード
* @param string $sig 比較用ハッシュ
* @return bool 一致した場合true
*/
public function validateHash($code, $sig)
{
$hash = hash('sha256', $code, true);
$len = strlen($hash) / 2;
$half = substr($hash, 0, $len);
$vsig = $this->ci->jwt->encodeBase64Uri($half);
return ($sig === substr($vsig, 0, strlen($sig)));
}
ユーザー属性をリクエストする
IDトークンのデコードと検証が成功したら、ユーザー属性(ユーザー情報)をリクエストします。
ユーザー属性は userinfo エンドポイントへリクエストします。このときアクセストークンが必要になります。
Login.php
public function yahoo()
{
// 認証コードの確認
...省略...
// stateが一致することを確認する
...省略...
// tokenエンドポイントへアクセストークンのリクエスト
...省略...
// IDトークンを復号化する
...省略...
// アクセストークンでユーザー属性をリクエストする
$user = $this->yahoo_library->requestUserInfo($token['access_token'], $endpoint['userinfo']);
// ログイン完了、結果を表示する
$_token = $token;
unset($_token['id_token']);
$viewdata = [
'message' => 'Yahooでログインしました。',
'data' => [
'token' => $_token,
'user' => $user,
'id_token' => $payload,
],
];
$this->load->view('logined', $viewdata);
}
ユーザー属性をリクエストするコードは、Yahoo_library ライブラリの requestUserInfo メソッドに記述します。
ユーザー属性をリクエストする(requestUserInfoメソッドの作成)
requestUserInfoメソッドは以下のようになります。
Yahoo_library.php
/**
* ユーザー属性情報をリクエストする
* @param string $accessToken アクセストークン
* @param string $endpoint 属性取得エンドポイント
* @return array 属性の配列
*/
public function requestUserInfo($accessToken, $endpoint)
{
// ヘッダーの指定
$header = [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accessToken],
];
// リクエスト
$response = $this->ci->curl_library->execPostCurl($endpoint, null, $header);
return $response['response'];
}
アクセストークンは curl のヘッダーに入れます。以下のような形式で入れる必要があります(POSTの場合)。
Authorization: Bearer ABCD123
以上でYahooでのログインが完了です。