まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:token

Github の認証ルールが変わったらしく、最終的には 2021 年 8 月 13 日からベーシック認証を用いた利用ができなくなるようです:
Token authentication requirements for Git operations


普段から Github は頻繁に使っていて、しかも現在はベーシック認証中心に使っていました。突然利用できなくなると困るので、早めの対策を取っておきました。その手順を紹介します。 また合わせて Github の2段階認証を利用すべく、IBM Security Verify アプリを使った2段階認証対応手順を紹介します。

【Github のパーソナルアクセストークン認証】
Github のベーシック認証の利用ができなくなる前にパーソナルアクセストークンと呼ばれるトークンを使った認証に切り替える必要があります。まずはその手順を紹介します。

Github ページにログインし、画面右上のアイコンをクリックして "Settings" を選択します:
2021031301


プロフィールページが表示されるので、画面左のサイドメニューから "Developer settings" を選択します:
2021031302


開発者向け設定ページが表示されます。ここでも画面左のサイドメニューから "Personal access tokens" を選択します:
2021031303


設定済みのパーソナルアクセストークン一覧画面が表示されます。新しいパーソナルアクセストークンを作成するために "Generate new token" を選択します:
2021031304


新たに作成するパーソナルアクセストークンの説明(名前)を Note に入力し、スコープは全てにチェックを入れます:
2021031305


最後に "Generate token" ボタンをクリックしてパーソナルアクセストークンを作成します:
2021031306


新しいパーソナルアクセストークンが生成されました。このトークン文字列はこの作成直後のタイミングのみ表示されます(別のページに推移したら2度と表示できません)。このタイミングでコピーするなどして値を保存しておくことを推奨します:
2021031307


これでパーソナルアクセストークンを生成することができました。これ以降で git push するなど認証が必要になった場合は、ID としてユーザー名、パスワードとして(これまでのログインパスワードではなく)パーソナルアクセストークン文字列を指定して認証することができるようになります。また 2021 年 8 月 13 日以降は(ベーシック認証が利用できなくなるため)このパーソナルアクセストークン文字列を使った認証のみがサポートされるようになります:
2021031308


【Github の2段階認証】
記事を分けて紹介することにしました。こちらを参照ください:
IBM Security Verify アプリを使って Github の2段階認証を設定する


Node.js のアプリケーションで、以下のような処理を実装してみました:
 - 認証(ログイン)用の API には誰でもアクセスできる
 - 認証 API では ID とパスワードを与えて認証し、正しいユーザーにはトークンを発行する
 - 認証以外の主な API はこの発行されたトークンを使ってアクセスした時だけ実行を許可する
 - API 呼び出し時にトークンがなかったり、正しくなかった場合は実行せずにエラー


この仕組を実現するために JSON Web Tokens (以下 "JWT")を使いました:
https://jwt.io/

2017081400


JWT はオープンかつ Node.js ではスタンダードなトークンベースの認証ライブラリです。以下で紹介するサンプルでは Web フレームワークである Express や、POST データを扱う body-parser も合わせて使うので、まとめてインストールしておきます:
$ npm install express body-parser jsonwebtoken

これらのライブラリを使って以下のようなアプリケーションを作成します:
//. app.js

var express = require( 'express' );
var app = express();
var bodyParser = require( 'body-parser' );
var jwt = require( 'jsonwebtoken' );

//. アプリケーションサーバーの稼働ポート番号
var port = process.env.PORT || 8080

//. 任意のシークレット文字列を登録
app.set( 'superSecret', 'welovenodejs' );  //. 任意の文字列

app.use( bodyParser.urlencoded( { extended: false } ) );
app.use( bodyParser.json() );

//. ユーザー情報(本来は DB などに格納された情報を使う)
var users = [
  { name: 'user0', password: 'pass0', admin: true },
  { name: 'user1', password: 'pass1', admin: false },
  { name: 'user2', password: 'pass2', admin: false },
  { name: 'user3', password: 'pass3', admin: false }
];

//. ドキュメントルートへの GET は許可
app.get( '/', function( req, res ){
  res.send( 'Hello. The API is at http://localhost:' + port + '/api' );
});

//. API ROUTES
var apiRoutes = express.Router();

//. トークンなしでアクセスを許可する API を先に定義する

//. POST(http://localhost:8080/api/authenticate)
apiRoutes.post( '/authenticate', function( req, res ){
  for( var i = 0; i < users.length; i ++ ){
    if( users[i].name == req.body.name && users[i].password == req.body.password ){
      //. 認証したユーザーの情報を使ってトークンを生成
      var token= jwt.sign( users[i], app.get( 'superSecret' ), {
        expiresIn: '24h'
      });
      res.json( { success: true, message: 'Authentication successfully finished.', token: token } );
      return;
    }
  }

  res.json( { success: false, message: 'Authentication failed.' } );
  return;
});

//. ここより上で定義した API には認証フィルタはかけていない(そのまま使える) //. 認証フィルタ apiRoutes.use( function( req, res, next ){
//. ポスト本体、URLパラメータ、HTTPヘッダいずれかにトークンがセットされているか調べる var token = req.body.token || req.query.token || req.headers['x-access-token']; if( !token ){
//. トークンが設定されていなかった場合は無条件に 403 エラー return res.status(403).send( { success: false, message: 'No token provided.' } ); } //. 設定されていたトークンの値の正当性を確認 jwt.verify( token, app.get( 'superSecret' ), function( err, decoded ){ if( err ){ //. 正当な値ではなかった場合はエラーメッセージを返す return res.json( { success: false, message: 'Invalid token.' } ); } //. 正当な値が設定されていた場合は処理を続ける req.decoded = decoded; next(); }); }); //. 以下はトークンがないと使えない API //. GET(http://localhost:8080/api/) apiRoutes.get( '/', function( req, res ){ res.json( { message: 'Welcome to API routing.' } ); }); //. GET(http://localhost:8080/api/users) apiRoutes.get( '/users', function( req, res ){ res.json( users ); }); //. /api 以下に API をルーティング app.use( '/api', apiRoutes ); app.listen( port ); console.log( 'server started http://localhost:' + port + '/' );

肝になるのはルーティングに認証フィルタを定義している箇所です。ここよりも前(上)で定義した内容には認証フィルタは有効にならないので、認証なしで使える API となります(つまり GET / と POST /authenticate は認証していなくても使えます)。

この POST /api/authenticate API でポストデータ user, password を受取り、その値が変数 users の中で定義されているいずれかの組み合わせと一致していれば 24H 有効なトークンが発行されます(これを以下の API 実行時にパラメータ指定します)。

一方、ここよりも後(下)で定義する内容には認証フィルタが有効になり、GET /api と GET /api/users は上記方法で取得したトークンがパラメータに設定されていないと正しく処理されない API となります。


実際にこのアプリケーションを実行し($ node app)、curl コマンドで挙動を確認してみましょう。まずは問題なく実行できるはずの GET / を実行します:
$ curl -XGET 'http://localhost:8080/'
Hello. The API is at http://localhost:8080/api

問題なく実行できました。次は認証フィルタをかけた GET /api を実行してみます:
$ curl -XGET 'http://localhost:8080/api'
{"success":false,"message":"No token provided."}

API を実行した結果、「トークンがない」時のエラーメッセージが表示されました。ここも期待通りに動いています(試しませんが、ユーザー一覧を取得する GET /api/users も同様のエラーになります)。

では POST /api/authenticate で認証してトークンを取得してみますが最初はわざとパスワードを間違えてみます:
$ curl -XPOST -H 'Content-Type:application/json' 'http://localhost:8080/api/authenticate' -d '{"name":"user1","password":"pass0"}'
{"success":false,"message":"Authentication failed."}

先程同様にエラーになりましたが、エラーメッセージが「認証失敗」に変わりました。では改めて正しいパスワードを指定して実行してみます:
$ curl -XPOST -H 'Content-Type:application/json' 'http://localhost:8080/api/authenticate' -d '{"name":"user1","password":"pass1"}'
{"success":true,"message":"Authentication successfully finished.","token":"XXXXXX...XXXXXX"}

今度は認証が成功しました。レスポンスにも "token" が含まれています。最後にここで返ってきた token の値を指定して、先程アクセスできなかった GET /api を実行してみます:
$ curl -XGET 'http://localhost:8080/api?token=XXXXXX...XXXXXX'
{"message":"Welcome to API routing."}

今度は API が実行できました。同様にして GET /api/users も実行できるはずです:
$ curl -XGET 'http://localhost:8080/api/users?token=XXXXXX...XXXXXX'
[{"name":"user0","password":"pass0","admin":true},{"name":"user1","password":"pass1","admin":false},{"name":"user2","password":"pass2","admin":false},{"name":"user3","password":"pass3","admin":false}]


こんな感じで API の実行可否をトークンで制御できるようになりました。

今回の例ではソースコード内に静的に用意されたユーザー一覧を使って認証を行いましたが、実際の運用ではデータベース内に定義されたテーブル情報などを使うことになると思います。ただ JWT の基本的な考え方はこの1つのソースファイルだけで実現できているので、応用しやすいと思っています。

 

このページのトップヘ