Carpe Diem

備忘録

Node.jsでOpenID Connect認証

概要

前回OpenIDで認証を行いました。
ですが実はGoogleOpenIDでの認証は2015/04までに廃止の予定らしいのでOpenIDConnectへの移行を勧めてるらしいです。
なので今回はOpenIDConnectでの認証を実装します。

環境

  • Node.js 0.10.22
  • Express 4.0
  • passport 0.2.1

GoogleDevelopersConsoleでアプリ作成

OpenIDConnect = OAuth for login なので、ディベロッパーコンソールでアプリを作成します。 https://console.developers.google.com/project/

プロジェクト作成

f:id:quoll00:20141214192654p:plain

クライアントIDの作成

左メニューのAPIと認証認証情報をクリックしてください。

f:id:quoll00:20141214192710p:plain

クライアントIDの作成をクリック

f:id:quoll00:20141214192818p:plain

ウェブアプリケーションを選択して次へ進みます。

f:id:quoll00:20141214193052p:plain

最低限以下を設定します。

  • メールアドレス
  • サービス名

f:id:quoll00:20141214193145p:plain

次にコールバック先のURL(リダイレクトURL)を設定します。今回は下記の設定をします。

項目 説明 今回の例
Javascript生成元 サイトの URL (パスは含まない) http://localhost:3000/
リダイレクトURL 認証後のコールバック先 URL http://localhost:3000/oauth2callback

f:id:quoll00:20141214193222p:plain

設定が完了すると上記の画面が表示されます。このうち

  • クライアントID
  • クライアントシークレット
  • リダイレクトURL

は実装でも使用するのでメモしておいてください。

実装

事前準備

ケルトンコードを生成。

$ express openid-connect
$ cd openid-connect

次にライブラリを追加。今回はpassport-openidconnectを使います。

    "passport": "*",
    "passport-openidconnect": "*",
    "express-session": "*"

追加したらインストールします。

$ npm install

インストールしたら以下のコードをapp.jsに追記していきます。

ライブラリのインポート

var passport = require('passport');
var session = require('express-session');
var OpenidConnectStrategy = require('passport-openidconnect').Strategy;

app.use(passport.initialize());
app.use(passport.session());

passportの設定

  • clientID
  • clientSecret
  • callbackURL

に先ほどGoogleDevelopersConsoleで登録された値を設定します。またscopeは最低限openidが必要ですが、emailとかも必要であれば

  • openid
  • email
  • profile

あたりがよく利用されるので入れてもいいかもです。

passport.use(new OpenidConnectStrategy({
    authorizationURL: "https://accounts.google.com/o/oauth2/auth",
    tokenURL: "https://accounts.google.com/o/oauth2/token",
    userInfoURL: "https://www.googleapis.com/oauth2/v1/userinfo",
    clientID: "クライアントID",
    clientSecret: "クライアントシークレット",
    callbackURL: "http://localhost:3000/oauth2callback",
    scope: ["openid", "email", "profile" ]
}, function(accessToken, refreshToken, profile, done) {
    console.log('accessToken: ', accessToken);
    console.log('refreshToken: ', refreshToken);
    console.log('profile: ', profile);
    return done(null, profile);
}));

passport.serializeUser(function(user, done){
    done(null, user);
});

passport.deserializeUser(function(obj, done){
    done(null, obj);
});

本来OpenIDConnectならID Tokenが返されるべきですが、passportのインタフェースがOAuth2用なせいなのか戻り値は

  • accessToken
  • refreshToken
  • profile

になってますね。後々改善されるのでしょうか。

ルーティング

/auth/googleにアクセスしたら認証が走るようにします。

app.get('/auth/google', passport.authenticate('openidconnect'));

app.get('/oauth2callback', passport.authenticate('openidconnect', {
    failureRedirect: '/login'
}), function(req, res) {
    // Successful authentication, redirect home.
    res.redirect('/');
});

View

./views/index.jadeにリンクを付けます。

extends layout

block content
  h1= title
  p Welcome to #{title}
  a(href="/auth/google") Sign In with Google

以上で設定は完了です。起動しましょう。

$ ./bin/www

動作確認

http://localhost:3000にアクセスしてみます。

f:id:quoll00:20141214160617p:plain

リンクをクリックします。

f:id:quoll00:20141214160959p:plain

ログインに使う自分のアカウントを選択します。

f:id:quoll00:20141214194520p:plain

権限の承認です。承認します。

f:id:quoll00:20141214160617p:plain

認証が完了して戻ってきました。

コンソールログを見ると以下のように出力されます。

accessToken:  accounts.google.com
refreshToken:  102952998963950740274
profile:  { id: undefined,
  displayName: 'FooBar',
  name: { familyName: 'Foo', givenName: 'Bar', middleName: undefined },
  _raw: '{\n "id": "102952998963950740274",\n "email": "cicatrice@gmail.com",\n "verified_email": true,\n "name": "FooBar",\n "given_name": "Bar",\n "family_name": "Foo",\n "link": "https://plus.google.com/102952998963950740274",\n "picture": "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg",\n "gender": "male",\n "locale": "ja",\n "hd": "google.com"\n}\n',
  _json:
   { id: '102952998963950740274',
     email: 'cicatrice@gmail.com',
     verified_email: true,
     name: 'FooBar',
     given_name: 'Bar',
     family_name: 'Foo',
     link: 'https://plus.google.com/102952998963950740274',
     picture: 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg',
     gender: 'male',
     locale: 'ja',
     hd: 'google.com' } }

ここでのprofile._json.idは固有かつ不変の値なので、これをユニークIDとして認証に使うことができます。

ちなみにscopeがopenidのみの場合は以下のようになります。

accessToken:  accounts.google.com
refreshToken:  102952998963950740274
profile:  { id: undefined,
  displayName: 'FooBar',
  name: { familyName: 'Foo', givenName: 'Bar', middleName: undefined },
  _raw: '{\n "id": "102952998963950740274",\n "name": "FooBar",\n "given_name": "Bar",\n "family_name": "Foo",\n "link": "https://plus.google.com/102952998963950740274",\n "picture": "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg",\n "gender": "male"\n}\n',
  _json: 
   { id: '102952998963950740274',
     name: 'FooBar',
     given_name: 'Bar',
     family_name: 'Foo',
     link: 'https://plus.google.com/102952998963950740274',
     picture: 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg',
     gender: 'male' } }

以上です。お疲れ様でした。

注意

今回使ったpassport-openidconnectですが、本来OpenID Connectで必要な

  • 各クレーム(audnonce)の検証
  • 署名の検証

のどれもがごっそり抜けてます。なのでこのままだとOAuthを用いた認証と同じく脆弱性がある状態なので自前で実装する必要があります。

参考サイト