56
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Node.jsでOpenID ConnectのOPとRPを実装してみた

Last updated at Posted at 2017-06-14

OpenID Connectを使った最低限の枠組みを試しに作ってみた。
いろいろ認識間違っているところがある気がしているので 優しく 指摘していただければ……。

動機については以前の記事にて。
OpenID Connect対応の認証サーバ(OP/IdP)を作るために調べてみた

イメージとしては

  1. サービスサイトの認証ページにアクセス
  2. 認証プロバイダのログインページにアクセス
  3. サービスサイトに戻されて認証状態でサービスサイトを利用する

みたいな仕組みの枠組みを作ってみます。
ユーザ情報の検索などは全てダミーデータを使って代替しています。

念のため用語説明:

OP: OpenID Provider 認証する(プロバイダー)側
RP: Relying Party 認証してもらう(クライアント)側

使ったもの

Certifiedなライブラリを使いたかったのでpanva氏のライブラリを使用します。

OP側の実装

プロバイダー側は最低限でよければこんな感じ。

index.js
'use strict';

const Provider = require('oidc-provider');
const issuer = 'http://localhost:3030';
const config = {
};

// この部分は実際に使うときはDBから取得する
const clients = [
  {
    client_id: '0',
    client_secret: '0',
    scope: 'openid email',
    redirect_uris: [
      'http://localhost:3000/auth/cb'
    ]
  }
];

const oidc = new Provider(issuer, config);
oidc.initialize({ clients }).then(() => {
  console.log(oidc.callback);
  console.log(oidc.app);
  oidc.app.listen(3030);
});

clients配列の中のオブジェクトには

  • client_id
  • client_secret
  • scope
  • redirect_uris

が必須。

scopeに関しては指定できる値が決められている。最初にopenidの指定は必須。
ここの指定が変わることで必要な実装も変わるはずだが、そこまではまだ把握できていない。

redirect_urisはコールバック先として許可するURIを配列で保持する。RPからredirect_uriとして渡すコールバック先はここに指定したURIと完全に一致しないとエラーになる(末尾の/の有無も一致させる)。

RP側の実装

ベースはexpress-generatorで作成し、このコミットでの差分がRPとしての実装部分になります。

Express.jsを用いてクライアント側のサービスを書いてみましたが、アプリケーション全体を認証処理でまるっと囲ってあげる感じで実装すれば良さそうです。

ブートストラップ(bin/www)の修正

bin/www
var appFactory = require('../app');

// ...

const { Issuer } = require('openid-client');

// OPの探索処理
Issuer.discover('http://localhost:3030').then(issuer => {

  // 発見したissuerをapp.jsに渡してclientの取得処理を行う(後述)
  const app = appFactory(issuer);

  // 既存のbin/wwwの処理
  // ...

}).catch(err => {
  console.error(err);
  process.exit(1);
});

// スコープの関係で、変数serverを利用しているイベントロガー類を削除
// 実サービスに載せるときはちゃんと実装し直す必要がある

Issuer.discover()の引数にOP提供のURLを指定するだけで、OPの探索処理が実行されるのでちょっと驚いた。すごくお手軽。

コントローラ(app.js)の修正

まず全体の構成としてexpress-generatorで作成する雛形だと

var app = express();
// ...
module.exports = app;

という形で外部に提供されるが、先述の通り探索した結果のissuerを受け取りたいので、以下のようにissuerを引数とする関数を提供するようにする。

module.exports = (issuer) => {
  // 引数issuerを用いた初期化処理など
  // ...

  var app = express();

  // ...

  return app;
}

その他app.jsへの主要な修正は以下の通り。

app.js
module.exports = (issuer) => {

  const client = new issuer.Client({
    client_id: '0',
    client_secret: '0'
  });

  // redirect_uriはOPで定義したredirect_urisに含まれている値でないとエラーになる
  const params = {
    redirect_uri: 'http://localhost:3000/auth/cb'
  };

  // 'oidc'と名前をつけて認証ストラテジーを定義する
  passport.use('oidc', new Strategy({ client, params }, (tokenset, userinfo, done) => {
    console.log('tokenset', tokenset);
    console.log('access_token', tokenset.access_token);
    console.log('id_token', tokenset.id_token);
    console.log('claims', tokenset.claims());
    console.log('userinfo', userinfo);

    // プロバイダー側で入力したユーザIDがtokenset.claims.subに格納されているので
    // 実運用時はこの部分でユーザ検索して存在チェックをする
    // ここで比較するのはユーザー名ではなく、認証サーバーで識別されるユーザーID
    if (tokenset.claims().sub !== '0') {
      return done(null);
    }
    // 存在するユーザーで認証されている場合はユーザー情報を返す
    // 返した情報は`req.user.client_id`に格納される
    return done(null, userinfo);

  }));

  var app = express();

  // ...

  // express-sessionの定義
  app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: false
  }));

  // passport利用開始の準備
  app.use(passport.initialize());
  app.use(passport.session());

  // ...

  // 認証ページへのルーティング
  app.get('/auth', passport.authenticate('oidc'));
  // プロバイダーからのコールバック先へのルーティング
  app.get('/auth/cb', passport.authenticate('oidc', {
    successRedirect: '/',          // 認証成功時の遷移先
    failureRedirect: '/users' // 認証失敗時の遷移先
  }));

  // ...

  return app;
}

// ユーザ情報をセッションに保存するためにpassportにシリアライザ、デシリアライザを定義する

passport.serializeUser(function(user, done) {
  // 実際の処理ではセッション情報にはIDなどキーとなる情報のみ保存する
  done(null, user);
});

passport.deserializeUser(function(id, done) {
  // 実際の処理ではセッション情報にもつIDなどからユーザ情報を取得する
  done(null, {client_id: id});
});

express-sessionの定義部分については「Node.js + Express.js + express-sessionでセッションにデータ格納する方法」もご参照ください。

起動と動作確認

  1. OPをnode index.jsで起動
  2. RPをnpm startで起動
    • bin/wwwでOPを探索しているので起動時にOPが起動していないとエラーで起動できない
  3. RPの認証ページhttp://localhost:3000/authに遷移
  4. OPの認証ページhttp://localhost:3030に飛ばされる
    • ID/PASSをそれぞれ 0 / 0 で認証する
  5. RPのコールバック先http://localhost:3000/auth/cbに遷移して(見た目にはわからない)、認証成功していればhttp://localhost:3000/にリダイレクトされる

まだよくわかっていない部分

  • プロバイダー側とクライアント側でclient_id, client_secretをどこでマッチングしているのか
  • パラメータscopeの違いによる実装内容、挙動の違い
  • client_id, client_secretはクライアントサービスごとに定義されているっぽいが、クライアントサービス内で管理しているユーザの扱い
56
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?