Keycloak を用いた認証
お次は認証・認可処理を追加したいと思います。ここでは Keycloak というサーバアプリケーションを利用します。
Keycloak は SSO 機構を提供します。また、OAuth2 や OpenID Connect など様々な方式をサポートしているほか、Twitter などのソーシャルログイン機能も用意されているなど、なかなか高機能です。今回はこの Keycloak を用いて lifelog の POST/PUT/DELETE の API に認証をかけます。
もちろん WildFly Swarm では通常の Java EE アプリケーション同様、JAAS や Apache Shiro といったライブラリを利用することで認証・認可を実現できます。また、まだアプリケーションは lifelog の 1 つしかありませんのでそういう意味でもちょっとオーバーエンジニアリングというか、大げさかもしれません。しかし、今後同じ認証・認可情報を用いたサービスを作ることも踏まえて、ここでその基盤を据えておきたいと思います。
Keycloak を利用した場合の大ざっぱな仕組みとしては以下のようなものです。
- Keycloak サーバに TOKEN をもらう
- 1 でもらった TOKEN をヘッダにつけて lifelog の API をリクエストする
ここではすでに用意した Keycloak の設定ファイル(keycloak.json/lifelog-realm.json)を利用しますので、以下からダウンロードしそれぞれ配置してください。
- keycloak.json(src/main/resources 配下)
- lifelog-realm.json(プロジェクト直下)
ご自分で設定ファイルを作成してみたい場合は 付録 Keycloak の設定 を参照ください。
Keycloak の実行
Keycloak サーバは実体としては Java EE の Web アプリケーションで、WildFly にデプロイして利用します。なので、既存の WildFly にデプロイしたり、コミュニティから提供されている WildFly 込のものを用いて起動可能です。また Docker イメージも存在します。また、以下のように WildFly Swarm では Keycloak Server 用 Fraction を用意していますので、lifelog と同様に uber jar を簡単に作成することもできます。
Maven リポジトリからダウンロードしてすぐ使えるものもあります。
https://wildfly-swarm.gitbooks.io/wildfly-swarm-users-guide/content/v/2017.1.1/server/keycloak.html
今回は Docker イメージを利用していきたいと思います。さっそくお呼びいたしましょう。
$ docker run -it -d \
--name lifelog-auth \
-p 18080:8080 \
-v `pwd`:/tmp \
jboss/keycloak:2.1.0.Final \
-b 0.0.0.0 -Dkeycloak.migration.action=import -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=/tmp/lifelog-realm.json
Keycloak の Admin ユーザを admin/admin というユーザ名/パスワードで設定しています。また、ホストの 18080 ポートから -> コンテナの 8080 へポートフォワードしています。
一番最後に渡している lifelog-realm.json というのはこちらで用意した Keycloak の設定ファイルです。起動時にインポートすることができます。
Keycloak による認証を用いた API へのアクセス
Keycloak が起動できたら、lifelog の方で認証・認可処理を追加していきます。
完成版は以下リポジトリにありますので、適宜参照ください。
https://github.com/emag/wildfly-swarm-tour/tree/2017.1.1/code/keycloak
まず、pom.xml に以下を追加します。
<dependency>
<groupId>org.wildfly.swarm</groupId>
<artifactId>keycloak</artifactId>
</dependency>
上記依存性を追加すると org.wildfly.swarm.keycloak.Secured
が利用できるようになるので、以下の処理を LifeLogDeployment#deployment()
に追加します。
import org.wildfly.swarm.keycloak.Secured;
[...]
archive.as(Secured.class)
.protect("/entries/*")
.withMethod("POST", "PUT", "DELETE")
.withRole("author");
これは /entries
以下のリソースに対する POST/PUT/DELETE
メソッドによるアクセスは author
ロールを持ったユーザのみ、という意味になります。
メソッドの実体としては上記内容で web.xml のセキュリティ関連の設定を組み立てているだけです。 ドキュメントも以下にあります。
また、Keycloak のクライアント側の設定ファイルとして keycloak.json がありますので、これを src/main/resources
配下に置いておきます(すでに冒頭で置いたものです)。
keycloak.json はセキュリティ設定の realm や Keyclaok サーバの URL などを設定します。
この keycloak.json はクラスパス直下においてあると WildFly Swarm が自動で読み込んでくれるのですが、Arquillian によるテスト時には読まれないため、アーカイブに追加するコードを記述しておきます。
archive.addAsWebInfResource(
new ClassLoaderAsset("keycloak.json", Bootstrap.class.getClassLoader()),
"keycloak.json");
先ほど配置した keycloak.json の内容を見ると、以下のようになっています。
{
"realm": "lifelog",
"realm-public-key": "...",
"bearer-only": true,
"auth-server-url": "${keycloak.auth-server-url}",
"ssl-required": "external",
"resource": "lifelog"
}
ほとんどの値はハードコードされていますが、Keycloak Server の URL を表す auth-server-url
はシステムプロパティ keycloak.auth-server-url
から設定されるようにしています。
この値はステージによって異なることがあるため、以下のように project-stages.yml で設定することにします。
swarm:
datasources:
data-sources:
lifelogDS:
driver-name: h2
connection-url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=TRUE
user-name: sa
password: sa
# 追記ここから
keycloak:
auth-server-url: http://localhost:18080/auth
# 追記ここまで
---
project:
stage: it
[...]
では上記変更をふまえて lifelog をビルド・実行し、アクセスしてみましょう(Keycloak Server を 18080 ポートで起動しておくことを忘れずに)。
$ ./mvnw clean package && \
java -jar target/lifelog-swarm.jar \
-Dswarm.project.stage=production
PostgreSQL を使わない場合は -Dswarm.project.stage=default にするか、このシステムプロパティを渡さないようにしてください
さっきまでと同じようにアクセスすると、以下のように 401 が返ってきます。
$ curl -X POST -H "Content-Type: application/json" -d '{"description" : "test"}' localhost:8080/entries -v
[...]
< HTTP/1.1 401 Unauthorized
[...]
<html><head><title>Error</title></head><body>Unauthorized</body></html>
しからばと、トークンを取りに行きましょう。下記のような形で TOKEN(access_token) として覚えておきます。
$ RESULT=`curl --data "grant_type=password&client_id=curl&username=user1&password=password1" http://localhost:18080/auth/realms/lifelog/protocol/openid-connect/token`
$ TOKEN=`echo $RESULT | sed 's/.*access_token":"//g' | sed 's/".*//g'`
RESULT では curl
として用意しておいたクライアント id でトークンを取りに行っています。この際 lifelog realm として、user1/password1 というユーザ名/パスワードをもったユーザがいるため、この情報を利用しています。RESULT として入っている情報はいろいろプロパティがついていますが、認証に必要なのは access_token
のみなので、これだけもらって TOKEN にしまっています。
realm は Keycloak での認証の単位です。ここでは lifelog という realm を作っておきました。 lifelog realm の内容は lifelog-realm.json 内で定義されており、user1 の存在も確認できます。
あらためて Authorization ヘッダにトークンを渡して POST します。
$ curl -X POST -H "Content-Type: application/json" -H "Authorization: bearer $TOKEN" -d '{"description" : "test"}' localhost:8080/entries -v
[...]
< HTTP/1.1 201 Created
[...]
< Location: http://localhost:8080/entries/1
いいですね。ちなみに TOKEN は 5 分で切れるのでお急ぎください。
認証を含んだ IT の実施
せっかくなので EntryControllerIT もこの認証に対応しておきましょう。
まずは PostgreSQL と同じように、IT 実行前に Keycloak Server を起動するようにしておきます。pom.xml の it プロファイルに以下を追記します。Keycloak Server は 28080 ポートでフォワードすることとします。
<properties>
[...]
<version.keycloak-server>2.1.0.Final</version.keycloak-server>
</properties>
[...]
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${version.docker-maven-plugin}</version>
<configuration>
<images>
<image>
[... PostgreSQL の設定 ...]
</image>
<!-- ここから追記 -->
<image>
<alias>lifelog-auth</alias>
<name>jboss/keycloak:${version.keycloak-server}</name>
<run>
<ports>
<port>28080:8080</port>
</ports>
<volumes>
<bind>
<volume>${project.basedir}:/tmp</volume>
</bind>
</volumes>
<cmd>
-b 0.0.0.0 -Dkeycloak.migration.action=import -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=/tmp/lifelog-realm.json
</cmd>
<wait>
<log>WFLYSRV0025</log>
<time>20000</time>
</wait>
<log>
<prefix>LIFELOG_AUTH</prefix>
<color>cyan</color>
</log>
</run>
</image>
<!-- ここまで追記 -->
</images>
</configuration>
[...]
</plugin>
次に、lifelog.api.EntryControllerIT
のテスト内容を修正します。
まず最初の方に以下のようなトークン取得処理を追加します。
import javax.ws.rs.core.Form;
[...]
// (1)
String keycloakUrl = System.getProperty("auth.url") + "/realms/lifelog/protocol/openid-connect/token";
Client client = ClientBuilder.newClient();
WebTarget target = client.target(keycloakUrl);
// (2)
Form form = new Form();
form.param("grant_type", "password");
form.param("client_id", "curl");
form.param("username", "user1");
form.param("password", "password1");
// (3)
Token token = target.request(MediaType.APPLICATION_JSON).post(Entity.form(form), Token.class);
// 以降、既存のコード
UriBuilder baseUri = UriBuilder.fromUri(deploymentUri).path("entries");
// 上で Client と WebTarget を宣言したので、変数代入のみに変更
client = ClientBuilder.newClient();
target = client.target(baseUri);
[...]
Keycloak の URL は IT 用のものをシステムプロパティから渡されるようにしておきます(1)。
curl でやっていたことと同じことをやればよいので上記のような感じでトークンが取得できます(2)。
なんとなくトークン格納用の Token クラスも用意。とりあえず必要な access_token
だけ取るようにしました(3)。
package lifelog.api;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class Token {
@JsonProperty("access_token")
private String accessToken;
}
あとは認証が必要な POST と DELETE でこのトークンを渡してやればよいですね。
Response response = target.request()
.header("Authorization", "bearer " + token.getAccessToken())
.post(Entity.json(entry));
[...]
response = target.request()
.header("Authorization", "bearer " + token.getAccessToken())
.delete();
以下コマンドで実行します。前述の通り IT 用の Keycloak の URL をシステムプロパティ(auth.url
)として渡しておく必要があります。
$ ./mvnw clean verify \
-Dswarm.project.stage.file=file://`pwd`/project-stages.yml \
-Dswarm.project.stage=it \
-Dauth.url=http://localhost:28080/auth \
-Pit
auth.url
と実行時にわざわざ渡しているのは、testable=false
の Arquillian のテストの場合、test 実行側は project-stages.yml のシステムプロパティが見えないからです
うまくいきました? 余裕があればトークンなしや不正なトークンでリクエストすると 401 が出ることを確認するテストをしてみてもいいですね。
注意点
docker-maven-plugin の設定では、以下のように Keycloak Server が起動したことを表す WFLYSRV0025
を含むログが出力されるまで待つようにしています。
<wait>
<log>WFLYSRV0025</log>
<!-- 単位は ミリ秒 -->
<time>20000</time>
</wait>
ここではタイムアウト値として <time>
要素に 20000 ミリ秒設定しているのですが、もし この時間以内に起動しないと以下のようなエラーとなります。
[ERROR] DOCKER> [jboss/keycloak:2.1.0.Final] "lifelog-auth": Timeout after 20071 ms while waiting on log out 'WFLYSRV0025'
上記のようなエラーが出たら、<time>
要素の値を 60000 など増やしてみてください。