はじめに

西日本テクノロジー&イノベーション室2年目の山田です。約1年半のあいだ、Javaのアプリケーション開発業務を行ってきました。

先日、業務でOpenID ConnectのRelying Party(認証を要求する側)を実装をする機会がありました。その中で使うランダムな値の生成方法について調べていて、調査方法と実装方法に悩んだ部分がありました。そこで、自分が調査した流れと採用した実装方法、それらから得た学びをまとめました。

OpenID Connectの概要

OpenID Connect 1.0 は, OAuth 2.0 [RFC6749] プロトコルの上にシンプルなアイデンティティレイヤーを付与したものである.

OpenID Connect Core 1.0 日本語訳より引用)

OpenID Connectを利用したサービスでは、ユーザがOpenID ProviderのログインID、パスワードを保持するだけで複数のサービスを利用できるなどのメリットがあります。

実装するにあたって悩んだこと

OpenID Connectでは、CSRFへの対策としてstateというパラメーターが存在します。また、リプレイアタックへの対策としてnonceというパラメーターが存在しています。この2つのパラメーターを使用することで、より安全な通信を行うことが可能です。

このstate、nonceの2つの値の生成方法に悩んだのでお話します。

stateとnonceの生成方法の調査をする

まずはOpenID Connect Core 1.0を読む

stateとnonceがどのような値であるべきなのか、OpenID Connect Core 1.0にはこう書かれています。

項目 説明(OpenID Connect Core 1.0 3.1.2.1. Authentication Requestより引用)
state RECOMMENDED.リクエストとコールバックの間で維持されるランダムな値.一般的にCross-Site Request Forgery (CSRF, XSRF) 対策の目的で利用される,ブラウザCookieと紐づく暗号論的にセキュアな値を取る.
nonce OPTIONAL.ClientセッションとID Tokenを紐づける文字列であり,リプレイアタック対策に用いられる.この値はAuthentication Requestで指定され,そのままの値でID Tokenに含まれる.nonce値には,推測不可能なように十分なエントロピーを持たせること (MUST).

以上から、stateは「暗号論的にセキュア」で、nonceは「推測不可能なよう充分なエントロピーを持った」値であれば良いということが分かりました。

OpenID Connect Core 1.0には値が満たすべき前提条件は記載されていますが、具体的な実装方法については記載がありません。そのため、実装方法を自分で調べる必要があります。

Javaで実装する上での選択肢を探す

今回はJavaを利用したプロジェクトだったため、上記の値をJavaで作成する必要があります。 Javaでセキュアかつランダムな値を生成する方法としてはjava.security.SecureRandomを使用する方法が存在します。 上記クラスは、APIドキュメントにも

このクラスは暗号用に強化された乱数ジェネレータ(RNG)を提供します。

とあり、stateとnonceの性質を満たしていることがわかります。

しかし、他により良い選択肢がないか、一般的にはどのような実装方法が採用されていることが多いのかという疑問を持ちました。

他に選択肢がないか探す

そこで、私は社内の技術QAサイトを利用し、より良い方法がないか質問することにしました。 その結果、java.util.UUIDを利用することが多いこと、OpenID Certifcationで公式に認定されているOpenID ProviderやRelying Partyのソースコードが存在することを教えてもらいました。

採用した実装方法

今回はjava.util.UUIDを利用して値を生成する方法を採用しました。UUID version 4を取得するUUID.randomUUIDメソッドを使用することで、ランダムな文字列を取得できます。

java.util.UUIDを選んだ理由

java.util.UUIDOpenID Certifcationで認定されている、アイデンティティおよびアクセス管理ソフトウェアのKeycloakのソースコードでも使用されていました。公開されているソースコードは指定されたテストに合格し、OpenID Connectのプロトコルに準拠しているソースコードです。このことからjava.util.UUIDの使用はセキュリティ上の問題がないことを改めて確認できました。

また、社内の技術QAサイトでいただいた回答から、別のプロジェクトで類似した条件の値の生成にjava.util.UUIDを使用した実績があることもわかりました。

実際に以下のようなコードを記述して、ランダムな値を取得しました。

import java.util.UUID;
 ...(省略)... 
String num = UUID.randomUUID().toString();

java.util.UUIDにはStringオブジェクトを返すUUID.toStringメソッドが用意されており、ランダムな文字列を簡潔に取得できました。

java.util.UUIDとjava.security.SecureRandomの比較

今回はjava.util.UUIDを使用しランダムな文字列を生成することにしました。では、java.security.SecureRandomを採用した場合とjava.util.UUIDを採用した場合ではどのような違いがあるのでしょう。

java.util.UUIDのソースコードを確認すると、UUID.randomUUIDの実装でjava.security.SecureRandomが使用されていました。生成された乱数にさらにバイト配列の操作を行うことで、UUIDを生成していることが確認できました。

java.security.SecureRandomを使用した実装では、APIドキュメントに説明があるとおり、バイト配列を意識した操作を記述する必要があります。

SecureRandomの一般的な呼出し側は、次のメソッドを呼び出してランダム・バイトを取得します。 SecureRandom random = new SecureRandom(); byte bytes[] = new byte[20]; random.nextBytes(bytes);

APIドキュメントより引用)

java.util.UUIDと比較すると、意識しなければならない情報が多いです。java.security.SecureRandomを使用して実装した場合は、使用方法が正しいかのレビューやテストも必要なためjava.util.UUIDよりも実装時の作業量が多くなります。

ここまでで比較した条件を、以下の表にまとめました。

評価項目 SecureRandom UUID
予測の困難さ
実装の容易さ
可読性

今回の目的(安全かつランダムな文字列を生成する)においては、java.util.UUIDを使用した方がより簡潔にコードを記述して検証も行えるため、最適な選択ができたのでは、と考えています。

おわりに

今回はOpenID ConnectのRelying Party側の実装を行うにあたって必要な、ランダムな文字列を生成する方法について調査し、実装方法を選択しました。

目的を達成するために考えられる手法は複数あり、今回はjava.util.UUIDを選択して実装しました。

OpenID Connect Core 1.0だけでなく、OpenID Certificationの実装や社内での採用実績を参考に選択肢を比較することで、自分が選択した方法の根拠を確かにできたと思っています。


本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。