OpenID Connect 1.0 Implicit Flow OPを実装してみる

# はじめに
Enterprise Identity WGが『OpenID ConnectとSCIMのエンタープライズ利用ガイドライン』と『(同)実装ガイドライン』を公開しました。
https://www.openid.or.jp/news/2016/03/eiwg-guideline.html
非常に読み応えのある内容で、実装ガイドラインにはRubyによる実装例も公開されていました。
https://github.com/openid-foundation-japan/eiwg-guideline-samples

この記事ではJavaによる実装例を紹介します。

# 実行環境
下記バージョンで動作確認しています。
- Java 8(1.8.0_91)
- Tomcat 8(8.0.33)
- JSR 353: Java API for JSON Processing(1.0.4)
- JSON Web Token for Java and Android(0.6.0)
- FasterXML Jackson(2.7.0)
- Apache Commons Lang(3-3.4)
- Apache Log4j 2(2.5)

# 実装方針
シンプルにSERVLET/JSPでJavaDBを利用しました。以下のライブラリを利用しました。
- servlet-api.jar
- derby.jar
- commons-lang3-3.4.jar
- jackson-annotations-2.7.0.jar
- jackson-core-2.7.0.jar
- jackson-databind-2.7.0.jar
- javax.json-1.0.4.jar
- jjwt-0.6.0.jar

以下のテーブルを作成します。
<pre>
create table profile(uid varchar(128) not null primary key, passwd varchar(256) not null, email varchar(256), phone_number varchar(256), name varchar(256), given_name varchar(256), family_name varchar(256), middle_name varchar(256), nickname varchar(256), address varchar(1024));
create table client(client_id varchar(128) not null primary key,  secret varchar(256) not null, scope varchar(256) not null, redirect_uri varchar(256));
create table session(uid varchar(128) not null, access_token varchar(256) not null primary key, issued_in timestamp not null, scope varchar(256) not null,  client_id varchar(256) not null);
</pre>

# ログ出力
ログ出力にはApache Log4j 2ライブラリを利用します。アクセスログ、エラーログはINFO、デバッグログはTRACEとします。
- log4j-api-2.5.jar
- log4j-core-2.5.jar
- log4j-web-2.5.jar

log4j2.json
<pre>
{
  "configuration": {
    "status": "off",
    "appenders": {
        "File": {"name": "File",
                 "fileName": "logs/myop.log",
                 "PatternLayout": {"pattern": "%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"}
        }
    },
    "loggers": {
        "root": {"level": "info",
                 "AppenderRef": {"ref": "File"}
        }
    }
  }
}
</pre>

# DISCOVERY
/.well-known/openid-configurationにアクセスが来たら以下のファイルを表示します。
<pre>
openid-configuration.json
</pre>

/jks_uriにアクセスが来たら以下のファイルを表示します。
<pre>
jks_uri.json
</pre>

# AUTHORIZE
Implicit Flowの下記パラメータを実装しています。
- response_type
- prompt
- login_hint
- max_age
- client_id
- redirect_uri
- scope
- state
- nonce

access_tokenは以下で生成します。DBのPRIMARY KEYにすることで一意性は保証されます。
<pre>
access_token = RandomStringUtils.randomAlphanumeric(32);
</pre>

at_hashは以下で生成します。
<pre>
md.update(access_token.getBytes());
cipher_byte = md.digest();
byte[] half_cipher_byte = Arrays.copyOf(cipher_byte, (cipher_byte.length / 2));
String at_hash = Base64.getEncoder().withoutPadding().encodeToString(half_cipher_byte);
</pre>
                   
秘密鍵は以下で読み込みます。
<pre>
path = context.getRealPath("/WEB-INF/private.der");
File filePrivateKey = new File(path);
FileInputStream fis = new FileInputStream(path);
byte[] encodedPrivateKey = new byte[(int) filePrivateKey.length()];
fis.read(encodedPrivateKey);
fis.close();
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
</pre>

id_tokenは以下で生成します。
<pre>
Calendar exp = Calendar.getInstance();
exp.add(Calendar.SECOND, access_token_time);
id_token = Jwts.builder().setHeaderParam("alg", "RS256").setHeaderParam("typ", "JWT").setHeaderParam("kid", kit).setIssuer(issuer).claim("at_hash",at_hash).setSubject(username).setAudience(client_id).claim("nonce",nonce).setSubject(username).setExpiration(exp.getTime()).setIssuedAt(Calendar.getInstance().getTime()).claim("auth_time",String.valueOf(Calendar.getInstance().getTime().getTime()).substring(0,10)).signWith(SignatureAlgorithm.RS256,privateKey).compact();
</pre>

# LOGIN
認証処理のために下記パラメータを実装しています。ログイン成功の場合はシングルサインオンのためにセッションで保持しておきます。
- username
- password

入力されたパスワードはハッシュ化してDBのパスワードと比較します。
<pre>
byte[] cipher_byte;
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(password.getBytes());
cipher_byte = md.digest();
String sha256_password = Base64.getEncoder().withoutPadding().encodeToString(cipher_byte);
</pre>

promptパラメータにconsentが含まれる場合は同意処理フラグconsentをtrueに設定します。

# CONSENT
同意処理のために下記パラメータを実装しています。
- consent
- scope
- openid
- profile
- email
- phone
- address

scopeを各パラメータのチェックボックスに分解します。
<pre>
＜input type="checkbox" name="openid" value="openid" checked="true"＞openid＜br＞
<% if (authorize.getScope().contains("profile")) { %>
＜input type="checkbox" name="profile" value="profile" checked="true"＞profile＜br＞
<% } if (authorize.getScope().contains("email")) { %>
＜input type="checkbox" name="email" value="email" checked="true"＞email＜br＞
<% } if (authorize.getScope().contains("phone")) { %>
＜input type="checkbox" name="phone" value="phone" checked="true"＞phone＜br＞
<% } if (authorize.getScope().contains("address")) { %>
＜input type="checkbox" name="address" value="address" checked="true"＞address＜br＞
<% } %><p>
</pre>

OKの場合は同意処理フラグconsentをfalseに設定してauthorizeにPOSTします。CANCELの場合はエラーをredirect_uriに#フラグメントで返します。

<pre>
＜input type="hidden" name="consent" value="false"＞
</pre>

# LOGOUT
セッションを無効化してログアウトページを表示します。
<pre>
HttpSession session = request.getSession(false);
if (session != null) {
    session.invalidate();
}
RequestDispatcher rd = ctx.getRequestDispatcher("/logout.jsp");
rd.forward(request, response);
</pre>

# ERROR
異常処理のために下記エラーを実装しています。エラーはredirect_uriに#フラグメントで返します。
<pre>
#error=access_denied&error_description=User%20authentication%20failed.
#error=invalid_scope&error_description=The%20scope%20value%20is%20not%20supported.
#error=unauthorized_clienti&error_description=Client%20authentication%20failed.
#error=unsupported_response_type&error_description==The%20response_type%20value%20%22nnn%22%20is%20not%20supported.
#error=invalid_request&error_description=redirect_uri%20is%20not%20valid.
#error=invalid_request&error_description=nonce%20is%20not%20valid.
</pre>

# ダウンロード
このリポジトリではOpenIDファウンデーション・ジャパン Enterprise Identity WG が公開した『OpenID ConnectとSCIMのエンタープライズ実装ガイドライン』に合わせたスクラッチ実装のソースコードを公開しています。

ASP.NET Identityに対応しました。(2020/4/18)
https://qiita.com/namikitakeo/items/0de598b8e43eb5b1ff94