OAuth徹底入門 セキュアな認可システムを適用するための原則と実践(Justin Richer Antonio Sanso 須田 智之 Authlete, Inc.)|翔泳社の本
  1. ホーム >
  2. 書籍 >
  3. OAuth徹底入門 セキュアな認可システムを適用するための原則と実践

OAuth徹底入門 セキュアな認可システムを適用するための原則と実践



翻訳
監修

形式:
書籍
発売日:
ISBN:
9784798159294
定価:
4,620(本体4,200円+税10%)
仕様:
B5変・464ページ
カテゴリ:
ネットワーク・サーバ
キーワード:
#ネットワーク・サーバ・セキュリティ,#データ・データベース,#システム運用,#Web・アプリ開発
シリーズ:
徹底入門
紙の書籍

OAuthは近年、WEBアプリケーションで使われる主要な認可プロトコルです。本書ではOAuthをどのようなプラットフォームでも適用できるように解説をしています。
本書は全体で16章あり、4つのパートに分割しています。パート1にあたる第1章と第2章はOAuth 2.0のプロトコルの概要を説明しており、基盤となる知識を得るための読み物としています。パート2は第3章から第6章までとなっており、OAuth 2.0のエコシステム全体をどのように構築するのかについて示しています。パート3は第7章から第10章までとなっており、OAuth 2.0のエコシステムにおけるさまざまな構成要素が持つ脆弱性について説明しており、その脆弱性をどのように回避するのかについて述べています。最後のパートは第11章から第16章までで構成されており、OAuth 2.0を核とした次の世代のプロトコルについて語っており、標準や仕様に関してOAuthの周辺の技術も踏まえて見ていき、最後に本書のまとめを行っています。

OAuth2.0の仕組みを把握し、構築への実用的な指針を徹底解説
OAuthを実サービスに適用する技術を4部構成で徹底的に解説します。

OAuthを実サービスに適用する技術を4部構成で徹底的に解説します。

第1部はじめの一歩

OAuth2.0のプロトコルがどのように機能するのか、そして、なぜそのような仕様になっているのかについて、全体像を学びます。

第2部OAuth2.0環境の構築

OAuth2.0のエコシステム全体(クライアント、保護対象リソース、認可サーバ)をゼロから構築します。

第3部OAuth2.0の実装と脆弱性

OAuth 2.0のエコシステムにおける構成要素が持つ脆弱性について、脆弱性をどのように回避するかについて説明します。

第4部さらなるOAuthの活用

OAuthプロトコルの中核となる部分からすこし離れて、その堅牢な核の周りに用意された拡張機能、プロファイル、補助的な構成要素の世界について学びます。

第1部 はじめの一歩
Chapter 1 OAuth 2.0とは何か?そして、なぜ気にかけるべきなのか?
 1.1 OAuth 2.0とは何か?
 1.2 古き悪しき手法~クレデンシャルの共有~
 1.3 アクセス権の委譲
 1.4 OAuth 2.0~良い点と悪い点、そして醜い点~
 1.5 OAuth 2.0ではないものは何か?
 1.6 まとめ

Chapter 2 OAuthダンス~OAuthの構成要素間の相互作用~
 2.1 OAuth 2.0プロトコルの概要~トークンの取得と使用~
 2.2 OAuth 2.0における認可付与の詳細
 2.3 クライアント、認可サーバー、リソース所有者、保護対象リソース
 2.4 トークン、スコープ、認可付与
 2.5 OAuthの構成要素間のやり取り
 2.6 まとめ

第2部 OAuth 2.0環境の構築
Chapter 3 シンプルなOAuthクライアントの構築
 3.1 認可サーバーへのOAuthクライアントの登録
 3.2 認可コードによる付与方式を使ったトークンの取得
 3.3 保護対象リソースへのトークンの使用
 3.4 アクセス・トークンのリフレッシュ
 3.5 まとめ

Chapter 4 シンプルなOAuthの保護対象リソースの構築
 4.1 HTTPリクエストからのOAuthトークンの解析
 4.2 データストアにあるトークンとの比較
 4.3 トークンの情報をもとにしたリソースの提供
 4.4 まとめ

Chapter 5 シンプルなOAuthの認可サーバーの構築
 5.1 OAuthクライアントの登録管理
 5.2 クライアントの認可
 5.3 トークンの発行
 5.4 リフレッシュ・トークンのサポートの追加
 5.5 スコープのサポートの追加
 5.6 まとめ

Chapter 6 実際の環境におけるOAuth 2.0
 6.1 認可における付与方式
 6.2 クライアントの種類
 6.3 まとめ

第3部 OAuth 2.0の実装と脆弱性
Chapter 7 よく狙われるクライアントの脆弱性
 7.1 一般的なクライアントのセキュリティ
 7.2 クライアントに対するCSRF攻撃
 7.3 クライアント・クレデンシャルの不当な取得
 7.4 リダイレクトURIの登録
 7.5 認可コードの不正な取得
 7.6 トークンの不正な取得
 7.7 ネイティブ・アプリケーションでのベスト・プラクティス
 7.8 まとめ

Chapter 8 よく狙われる保護対象リソースの脆弱性
 8.1 保護対象リソースの脆弱性とはどのようなものなのか?
 8.2 保護対象リソースのエンドポイントの設計
 8.3 トークンのリプレイ攻撃
 8.4 まとめ

Chapter 9 よく狙われる認可サーバーの脆弱性
 9.1 一般的なセキュリティ
 9.2 セッションの乗っ取り
 9.3 リダイレクトURIの不正操作
 9.4 クライアントのなりすまし
 9.5 オープン・リダイレクトによる脆弱性
 9.6 まとめ

Chapter 10 よく狙われるOAuthトークンの脆弱性
 10.1 Bearerトークンとは何か
 10.2 Bearerトークンの使用に関するリスクと考慮点
 10.3 どのようにBearerトークンを保護するのか?
 10.4 認可コード
 10.5 まとめ

第4部 さらなるOAuthの活用
Chapter 11 OAuthトークン
 11.1 OAuthにおけるトークンとは何か?
 11.2 JWT(JSON Web Token)
 11.3 JOSE(JSON Object Signing and Encryption)
 11.4 トークン・イントロスペクション(Token Introspection)
 11.5 トークン取り消し(Token Revocation)
 11.6 OAuthトークンのライフサイクル
 11.7 まとめ

Chapter 12 動的クライアント登録
 12.1 どのようにサーバーがクライアントのことを知るのか?
 12.2 実行時におけるクライアント登録
 12.3 クライアント・メタデータ
 12.4 動的な登録が行われたクライアントの管理
 12.5 まとめ

Chapter 13 OAuth 2.0を使ったユーザー認証
 13.1 なぜ、OAuth 2.0は認証プロトコルではないのか?
 13.2 OAuthと認証プロトコルとの関連付け
 13.3 OAuth 2.0ではどのように認証を使うのか?
 13.4 認証にOAuth 2.0を使用する際に陥りやすい落とし穴
 13.5 OpenID Connect
 13.6 シンプルなOpenID Connectシステムの構築
 13.7 まとめ

Chapter 14 OAuth 2.0を使うプロトコルとプロファイル
 14.1 UMA(User Managed Access)
 14.2 HEART(HEAlth Relationship Trust)
 14.3 iGov(international Government assurance)
 14.4 まとめ

Chapter 15 Bearerトークンの次にくるもの
 15.1 なぜBearerトークン以上のものが必要なのか?
 15.2 所有証明(Proof of Possession:PoP)トークン
 15.3 所有証明(PoP)トークンのサポートに関する実装
 15.4 TLSトークン・バインディング
 15.5 まとめ

Chapter 16 まとめと結論
 16.1 正しいツール
 16.2 重要な決定を行うこと
 16.3 さらに広がるエコシステム
 16.4 コミュニティ
 16.5 OAuthの未来
 16.6 まとめ

付録
 付録A 本書で使っているフレームワークについて
 付録B 演習で使うソースコード集

本書は付属データの提供はございません。

お問い合わせ

内容についてのお問い合わせは、正誤表、追加情報をご確認後に、お送りいただくようお願いいたします。

正誤表、追加情報に掲載されていない書籍内容へのお問い合わせや
その他書籍に関するお問い合わせは、書籍のお問い合わせフォームからお送りください。

利用許諾に関するお問い合わせ

本書の書影(表紙画像)をご利用になりたい場合は書影許諾申請フォームから申請をお願いいたします。
書影(表紙画像)以外のご利用については、こちらからお問い合わせください。

追加情報はありません。
この商品の「よくある質問」はありません。

ご購入いただいた書籍の種類を選択してください。

書籍の刷数を選択してください。

刷数は奥付(書籍の最終ページ)に記載されています。

現在表示されている正誤表の対象書籍

書籍の種類:

書籍の刷数:

本書に誤りまたは不十分な記述がありました。下記のとおり訂正し、お詫び申し上げます。

対象の書籍は正誤表がありません。

最終更新日:2024年09月04日
発生刷 ページ数 書籍改訂刷 電子書籍訂正 内容 登録日
1刷 059
真ん中のコード内下から2行目
3刷
redirect_uri: client.redirect_urls[0]
redirect_uri: client.redirect_uris[0]

小文字の「l(エル)」を「i(アイ)に修正しました。
※リフローEPUBの場合は「3.2.1 認可リクエストの送信」の3番目のコードが該当します。
2020.01.07
1刷 061
一番下のコード内下から2行目
3刷
redirect_uri: client.redirect_urls[0]
redirect_uri: client.redirect_uris[0]

小文字の「l(エル)」を「i(アイ)に修正しました。
※リフローEPUBの場合は「3.2.2 認可サーバーからのレスポンスの処理」の3番目のコードが該当します。
2020.01.07
1刷 064
3つ目のコードの1行目
3刷
if (req.query.state == state) {
if (req.query.state != state) {

※リフローEPUBの場合は、「3.2.3 stateパラメータを使ったサイトをまたいだ攻撃に対する保護の追加」の3番目のコードが該当します。
2020.01.24
1刷 067
2つ目のコード 下から4行目
5刷
res.ender('error', { error: 'Server returned response code: ' +
res.render('error', {error: 'Server returned response code: ' +

※リフローEPUBの場合、図3.5の後にある2つ目のコードが該当箇所になります。
2021.07.28
1刷 080
4.2節 1番目のコードブロック
nosql.one(function(token) {
  if (token.access_token == inToken) {
    return token;
  }
}, function(err, token) {
  if (token) {
    console.log("We found a matching token: %s", inToken);
  } else {
    console.log(’No matching token was found.’);
  }
  req.access_token = token;
  next();
  return;
});
nosql.one().make(function(builder) {
  builder.where('access_token', inToken);
  builder.callback(function(err, token) {
    if (token) {
      console.log("We found a matching token: %s", inToken);
    } else {
      console.log('No matching token was found.');
    };
    req.access_token = token;
    next();
    return;
  });
});
2024.09.04
1刷 100
5.1節 2番目のコードブロック(2行目の「`_.find`」が「`__.find`」になります)
var getClient = function(clientId) {
  return _.find(clients, function(client) {
    return client.client_id == clientId;
  });
};
var getClient = function(clientId) {
  return __.find(clients, function(client) {
    return client.client_id == clientId;
  });
};
2024.09.04
1刷 102
最初のコードブロック内のres.renderの第一引数
5刷
if (!client) { res.render('error, {error: 'Unknown client'}); ……
if (!client) { res.render('error', {error: 'Unknown client'}); ……

「'error,」を「'error', 」に修正します。

※リフローEPUBの場合、「5.2.1 認可エンドポイント」の3番目のコードが該当箇所になります。
2022.10.20
1刷 105
1つ目のコード内、下から1行目
3刷
reqid=tKVUYQSM}&approve=Approve
reqid=tKVUYQSM&approve=Approve

不要な } を削除しました。
※リフローEPUBの場合は、「5.2.2 クライアントの認可」の2番目のコードが該当します。
2019.05.07
1刷 112
囲み内の見出し、下から4行目
5刷
見出し トークンの中に何をたせるのか? 下から4行目 OAuth のトークンは JWT(JASON Web Token)や……
見出し トークンの中に何をたせるのか? 下から4行目 OAuth のトークンは JWT(JSON Web Token)や……

内容を更新しました(2023/02/01).

※リフローEPUBの場合、「5.3.2 認可付与のリクエストに関する処理」のコラムの見出しと本文の2番目の段落が該当箇所になります。
2022.10.20
1刷 115
5.4節 2番目のコードブロック
nosql.one(function(token) {
  if (token.refresh_token == req.body.refresh_token) {
    return token;
  }
}, function(err, token) {
  if (token) {
    対象のリフレッシュ・トークンが存在する場合の処理を行う
  } else {
    res.status(400).json({error: 'invalid_grant'});
    return;
  }
});
nosql.one().make(function(builder) {
  builder.where('refresh_token', req.body.refresh_token);
  builder.callback(function(err, token) {
    if (token) {
      対象のリフレッシュ・トークンが存在する場合の処理を行う
    } else {
      res.status(400).json({error: 'invalid_grant'});
      return;
    }
  });
});
2024.09.04
1刷 115
5.4節 3番目のコードブロック
if (token.client_id != clientId) {
  nosql.remove(function(found) {
    return (found == token);
  }, function () {} );
  res.status(400).json({error: 'invalid_grant'});
  return;
}
if (token.client_id != clientId) {
  nosql.remove().make(function(builder) {
    builder.where('refresh_token', req.body.refresh_token);
  });
  res.status(400).json({error: 'invalid_grant'});
  return;
}
2024.09.04
1刷 130
ページ下部のコード
5刷
nosql.insert({ access_token: access_token, client_id: client.clientId, ……
nosql.insert({ access_token: access_token, client_id: client.client_id, ……

※リフローEPUBの場合、「6.1.1 インプリシット付与方式」の6番目のコードが該当箇所になります。
2022.10.26
1刷 135
上のコード
5刷
nosql.insert({
   access_token: access_token,
   client_id: client.clientId,
   scope: rscope
});
nosql.insert({
   access_token: access_token,
   client_id: clientId,
   scope: rscope
});

※リフローEPUBの場合「6.1.2 クライアント・クレデンシャルによる付与方式」の2番目のコラム「スコープと付与方式(Grant Type)のすぐ上にあるコードが該当箇所になります。
2022.11.01
1刷 139
1つ目のコード内、最下行
2刷
grant_type=password}&scope=foo%20bar&username=alice&password=secret
grant_type=password&scope=foo%20bar&username=alice&password=secret

不要な } を削除しました。

※リフローEPUBの場合は、「6.1.3 リソース所有者のクレデンシャルによる付与方式」の1番目のコードが該当します。
2019.02.19
1刷 140
2つ目のコード、上から3行目
3刷
if (user)
if (!user) {

※リフローEPUBの場合は、「6.1.3 リソース所有者のクレデンシャルによる付与方式」の5番目のコードが該当します。
2019.05.07
1刷 140
3番目のコード、上から2行目
3刷
if (user.password = password)
if (user.password != password) {

!=の間違い。
※リフローEPUBの場合は、「6.1.3 リソース所有者のクレデンシャルによる付与方式」の6番目のコードが該当します。
2019.05.07
1刷 141
上から2つ目のコード
5刷
nosql.insert({
   access_token: access_token,
   client_id: client.clientId,
   scope: rscope
});
nosql.insert({
   refresh_token: refresh_token,
   client_id: client.clientId,
   scope: rscope
});
nosql.insert({
   access_token: access_token,
   client_id: clientId,
   scope: rscope
});
nosql.insert({
   refresh_token: refresh_token,
   client_id: clientId,
   scope: rscope
});

※リフローEPUBでは、「6.1.3 リソース所有者のクレデンシャルによる付与方式」の図6.4の上にあるコードが該当箇所になります。
2022.11.01
1刷 142
本文上から1行目
3刷
クトを生成しています。
成しています。

※リフローEPUBの場合、「6.1.3 リソース所有者のクレデンシャルによる付与方式」の8番目のコードの直後の段落が該当します。
2019.05.07
1刷 151
コード内、下から6行目
3刷
if (callbackData.state == localStorage.getItem('oauth-state'))
if (callbackData.state !== localStorage.getItem('oauth-state')) {

※リフローEPUBの場合、「6.2.2 ブラウザ内アプリケーション」の4番目のコードが該当します。
2019.05.07
1刷 172
7.3節 3番目のコードブロック
function handleAuthorizationRequestClick(ev) {
  if (!client.client_id) {
    $.ajax({
      url: authServer.registrationEndpoint,
      type: 'POST',
      data: client,
      crossDomain: true,
      dataType: ’json’
    }).done(function(data) {
      client.client_id = data.client_id;
      client.client_secret = data.client_secret;
    }).fail(function() {
      $('.oauth-protected-resource')
      .text('Error while fetching registration endpoint');
    });

    } else { …略
var protectedResource = 'http://localhost:9002/resource';

window.onload = function() {
   if (!client.client_id) {
    $.ajax({
      url: authServer.registrationEndpoint,
      type: 'POST',
      data: client,
      crossDomain: true,
      dataType: 'json'
    }).done(function(data) {
      client.client_id = data.client_id;
      client.client_secret = data.client_secret;
    }).fail(function() {
      $('.oauth-protected-resource')
          .text('Error while fetching registration endpoint');
    });
  }
}
2024.09.04
1刷 237,238
10.4節 2番目のコードブロック
var form_data = qs.stringify({
  grant_type: 'authorization_code',
  code: code,
  redirect_uri: client.redirect_uris[0],
  code_veryfier: code_veryfier
});
var form_data = qs.stringify({
  grant_type: 'authorization_code',
  code: code,
  redirect_uri: client.redirect_uri,
  code_verifier: code_verifier
});
2024.09.04
1刷 257
11.3節 1番目のコードブロック
// 先ほどまでのすべての整合性チェックはこのif文内で行われる
if (jose.jws.JWS.verify(
  inToken,
  new Buffer(sharedTokenSecret).toString(’hex’),
  [header.alg])) {
…略
}
// 先ほどまでのすべての整合性チェックはこのif文内で行われる
if (jose.jws.JWS.verify(
    inToken,
    Buffer.from(sharedTokenSecret).toString('hex'),
    [header.alg])) {
  …略
}
2024.09.04
1刷 268
11.4節 1番目のコードブロック
var inToken = req.body.token;
nosql.one(function(token) {
  if (token.access_token == inToken) {
    return token;
  }
}, function(err, token) {
  if (token) {
    var introspectionResponse = {
      active: true,
      iss: 'http://localhost:9001/',
      aud: 'http://localhost:9002/',
      sub: token.user ? token.user.sub : undefined,
      username: token.user ? token.user.preferred_username : undefined,
      scope: token.scope ? token.scope.join(’ ’) : undefined,
      client_id: token.client_id
    };
    res.status(200).json(introspectionResponse);
    return;
  } else {
    var introspectionResponse = {
      active: false
    };
    res.status(200).json(introspectionResponse);
    return;
  }
});
var inToken = req.body.token;
console.log('Introspecting token %s', inToken);
nosql.one().make(function(builder) {
  builder.where('access_token', inToken);
  builder.callback(function(err, token) {
    if (token) {
      var introspectionResponse = {
        active: true,
        iss: 'http://localhost:9001/',
        aud: 'http://localhost:9002/',
        sub: token.user ? token.user.sub : undefined,
        username: token.user ? token.user.preferred_username : undefined,
        scope: token.scope ? token.scope.join(' ') : undefined,
        client_id: token.client_id
      };
      res.status(200).json(introspectionResponse);
      return;
    } else {
      var introspectionResponse = {
        active: false
      };
      res.status(200).json(introspectionResponse);
      return;
    }
  });
});
2024.09.04
1刷 274,275
11.5節 2番目のコードブロック
var inToken = req.body.token;
nosql.remove(function(token) {
  if (token.access_token == inToken &&
      token.client_id == clientId) {
    return true;
  }
}, function(err, count) {
  res.status(204).end();
  return;
});
var inToken = req.body.token;
nosql.remove().make(function(builder) {
  builder.and();
  builder.where('access_token', inToken);
  builder.where('client_id', clientId);
  builder.callback(function(err, count) {
    console.log("Removed %s tokens", count);
    res.status(204).end();
    return;
  });
});
2024.09.04
1刷 313
12.4節 3番目のコードブロック
nosql.remove(function(token) {
  if (token.client_id == req.client.client_id) {
    return true;
  }
}, function(err, count) {
   console.log("Removed %s clients", count);
});

res.status(  204).end();
return;
nosql.remove().make(function(builder) {
  builder.where('client_id', clientId);
  builder.callback(function(err, count) {
    console.log("Removed %s tokens", count);
  });
});

res.status(204).end();
return;
2024.09.04
1刷 324
上から4行目
2刷
ここでは認可サーバーとリソース所有者をまとめて、それをひとつの構成要素であるアイデンティティ・プロバイダとして見ていきます。
ここでは認可サーバーと保護対象リソースをまとめて、それをひとつの構成要素であるアイデンティティ・プロバイダとして見ていきます。

※リフローEPUBでは、図13.2のすぐ上の段落が該当箇所になります。
2019.02.19
1刷 410,411
付録A 2番目のコードブロック
> node -v v4.4.1 > npm -v 2.15.1
> node -v v14.13.1 > npm -v 6.14.8
2024.09.04
1刷 420,421
付録B リストB.6
var getAccessToken = function(req, res, next) {
  var inToken = null;
  var auth = req.headers[’authorization’];
  if (auth && auth.toLowerCase().indexOf(’bearer’) == 0) {
    inToken = auth.slice(’bearer ’.length);
  } else if (req.body && req.body.access_token) {
    inToken = req.body.access_token;
  } else if (req.query && req.query.access_token) {
    inToken = req.query.access_token
  }

  console.log(’Incoming token: %s’, inToken);

  nosql.one(function(token) {
    if (token.access_token == inToken) {
      return token;
    }
  }, function(err, token) {
    if (token) {
      console.log("We found a matching token: %s", inToken);
    } else {
      console.log(’No matching token was found.’);
    }

    req.access_token = token;
    next();
    return;
  });
};
var getAccessToken = function(req, res, next) {
  var inToken = null;
  var auth = req.headers['authorization'];
  if (auth && auth.toLowerCase().indexOf('bearer') == 0) {
    inToken = auth.slice('bearer '.length);
  } else if (req.body && req.body.access_token) {
    inToken = req.body.access_token;
  } else if (req.query && req.query.access_token) {
    inToken = req.query.access_token
  }

  console.log('Incoming token: %s', inToken);

  nosql.one().make(function(builder) {
    builder.where('access_token', inToken);
    builder.callback(function(err, token) {
      if (token) {
        console.log("We found a matching token: %s", inToken);
      } else {
        console.log('No matching token was found.');
      };
      req.access_token = token;
      next();
      return;
    });
  });
};
2024.09.04
1刷 424,425
付録B リストB.10
} else if (req.body.grant_type == ’refresh_token’) {
  nosql.one(function(token) {
    if (token.refresh_token == req.body.refresh_token) {
      return token;
    }
  }, function(err, token) {
    if (token) {
      console.log("We found a matching refresh token: %s",
          req.body.refresh_token);

      if (token.client_id != clientId) {
        nosql.remove(function(found) {
          return (found == token);
        }, function () {} );

        res.status(400).json({error: ’invalid_grant’});
        return;
      }

      var access_token = randomstring.generate();

      nosql.insert({
        access_token: access_token,
        client_id: clientId
      });

      var token_response = {
        access_token: access_token,
        token_type: ’Bearer’,
        refresh_token: token.refresh_token
      };

      res.status(200).json(token_response);
      return;
    } else {
      console.log(’No matching token was found.’);
      res.status(400).json({error: ’invalid_grant’});
      return;
    }
  });
}
} else if (req.body.grant_type == 'refresh_token') {
  nosql.one().make(function(builder) {
    builder.where('refresh_token', req.body.refresh_token);
    builder.callback(function(err, token) {
      if (token) {
        console.log("We found a matching refresh token: %s",
            req.body.refresh_token);

        if (token.client_id != clientId) {
          nosql.remove().make(function(builder) {
            builder.where('refresh_token', req.body.refresh_token);
          });
          res.status(400).json({error: 'invalid_grant'});
          return;
        }

        var access_token = randomstring.generate();

        nosql.insert({
          access_token: access_token,
          client_id: clientId
        });

        var token_response = {
          access_token: access_token,
          token_type: 'Bearer',
          refresh_token: token.refresh_token
        };

        res.status(200).json(token_response);
        return;
      } else {
        console.log('No matching token was found.');
        res.status(400).json({error: 'invalid_grant'});
        return;
      }
    });
  });
} else if …略
2024.09.04
1刷 425
付録B リストB.11
app.post(’/introspect’, function(req, res) {
  var auth = req.headers[’authorization’];
  var resourceCredentials = decodeClientCredentials(auth);
  var resourceId = resourceCredentials.id;
  var resourceSecret = resourceCredentials.secret;
  var resource = getProtectedResource(resourceId);

  if (!resource) {
    console.log(’Unknown resource %s’, resourceId);
    res.status(401).end();
    return;
  }

  if (resource.resource_secret != resourceSecret) {
    console.log(’Mismatched secret, expected %s got %s’,
        resource.resource_secret, resourceSecret);
    res.status(401).end();
    return;
  }

  var inToken = req.body.token;.

  console.log(’Introspecting token %s’, inToken);

  nosql.one(function(token) {
    if (token.access_token == inToken) {
      return token;
    }
  }, function(err, token) {
    if (token) {
      console.log("We found a matching token: %s", inToken);

      var introspectionResponse = {
        active: true,
        iss: ’http://localhost:9001/’,
        aud: ’http://localhost:9002/’,
        sub: token.user ? token.user.sub : undefined,
        username: token.user ? token.user.preferred_username :
        undefined,
        scope: token.scope ? token.scope.join(’ ’) : undefined,
        client_id: token.client_id
      };

      res.status(200).json(introspectionResponse);
      return;
    } else {
      console.log(’No matching token was found.’);

      var introspectionResponse = {
        active: false
      };

      res.status(200).json(introspectionResponse);
      return;
    }
  });
});
app.post('/introspect', function(req, res) {
  var auth = req.headers['authorization'];
  var resourceCredentials = decodeClientCredentials(auth);
  var resourceId = resourceCredentials.id;
  var resourceSecret = resourceCredentials.secret;
  var resource = getProtectedResource(resourceId);

  if (!resource) {
    console.log('Unknown resource %s', resourceId);
    res.status(401).end();
    return;
  }

  if (resource.resource_secret != resourceSecret) {
    console.log('Mismatched secret, expected %s got %s',
        resource.resource_secret, resourceSecret);
    res.status(401).end();
    return;
  }

  var inToken = req.body.token;

  console.log('Introspecting token %s', inToken);

  nosql.one().make(function(builder) {
    builder.where('access_token', inToken);
    builder.callback(function(err, token) {
      if (token) {
        console.log("We found a matching token: %s", inToken);

        var introspectionResponse = {
          active: true,
          iss: 'http://localhost:9001/',
          aud: 'http://localhost:9002/',
          sub: token.user ? token.user.sub : undefined,
          username: token.user ? token.user.preferred_username : undefined,
          scope: token.scope ? token.scope.join(' ') : undefined,
          client_id: token.client_id
        };

        res.status(200).json(introspectionResponse);
        return;
      } else {
        console.log('No matching token was found.');

        var introspectionResponse = {
          active: false
        };

        res.status(200).json(introspectionResponse);
        return;
      }
    });
  });
});
2024.09.04
1刷 426,427
付録B リストB.12
app.post(’/revoke’, function(req, res) {
  var auth = req.headers[’authorization’];

  if (auth) {
    // Authorizationヘッダーをチェック
    var clientCredentials = decodeClientCredentials(auth);
    var clientId = clientCredentials.id;
    var clientSecret = clientCredentials.secret;
  }

  // ない場合はPOST送信されたボディーをチェック
  if (req.body.client_id) {
    if (clientId) {
      // すでにAuthorizationヘッダーにクライアントのクレデンシャルがある場合はエラー
      console.log(’Client attempted to authenticate with multiple methods’);
      res.status(401).json({error: ’invalid_client’});
      return;
    }

    var clientId = req.body.client_id;
    var clientSecret = req.body.client_secret;
  }

  var client = getClient(clientId);
  if (!client) {
    console.log(’Unknown client %s’, clientId);
    res.status(401).json({error: ’invalid_client’});
    return;
  }

  if (client.client_secret != clientSecret) {
    console.log(’Mismatched client secret, expected %s got %s’,
        client.client_secret, clientSecret);
    res.status(401).json({error: ’invalid_client’});
    return;
  }

  var inToken = req.body.token;

  nosql.remove(function(token) {
    if (token.access_token == inToken && token.client_id == clientId) {
      return true;
    }
  }, function(err, count) {
    console.log("Removed %s tokens", count);
    res.status(204).end();
    return;
  });
});
app.post('/revoke', function(req, res) {
  var auth = req.headers['authorization'];
  if (auth) {
    // Authorizationヘッダーをチェック
    var clientCredentials = decodeClientCredentials(auth);
    var clientId = clientCredentials.id;
    var clientSecret = clientCredentials.secret;
  }

  // ない場合はPOST送信されたボディーをチェック
  if (req.body.client_id) {
    if (clientId) {
      // すでにAuthorizationヘッダーにクライアントのクレデンシャルがある場合はエラー
      console.log('Client attempted to authenticate with multiplemethods');
      res.status(401).json({error: 'invalid_client'});
      return;
    }

    var clientId = req.body.client_id;
    var clientSecret = req.body.client_secret;
  }

  var client = getClient(clientId);
  if (!client) {
    console.log('Unknown client %s', clientId);
    res.status(401).json({error: 'invalid_client'});
    return;
  }

  if (client.client_secret != clientSecret) {
    console.log('Mismatched client secret, expected %s got %s',
        client.client_secret, clientSecret);
    res.status(401).json({error: 'invalid_client'});
    return;
  }

  var inToken = req.body.token;
  nosql.remove().make(function(builder) {
    builder.and();
    builder.where('access_token', inToken);
    builder.where('client_id', clientId);
    builder.callback(function(err, count) {
      console.log("Removed %s tokens", count);
      res.status(204).end();
      return;
    });
  });
});
2024.09.04

感想・レビュー

fakiyer さん

2019-03-30

OAuth2.0に関してサンプルコードとともにとても詳しく書かれている。 OpenID Connectについての説明もとても参考になった。 セキュリティに関しても詳しく書かれているので、OAuth構築する際は読んでおきたい本。

james さん

2020-05-23

OAuthを使うに当たってweb上の記事を読んでも用語の意味が分からず苦労したので体系的に理解したくて読んだ。OAuth2.0をきちんと使うなら読んでおくべき一冊だと思う。著者はOAuthに仕様検討段階から関わっているとのことで、なぜOAuth2.0が必要となったのかといった背景も含めて説明してくれる。中盤からはサンプルコードにプログラムを書き込んでいきながら進めるハンズオンでこれも理解を助けてくれた。node.jsは12ではエラーがでたので、8にバージョンを落としたら動作した。

kaseken さん

2020-05-10

技術書1000冊読破計画/17冊目