はじめに
OpenID Connect/OAuth2の認可サーバーとして広く使われているKeycloakですが、次のようなケースではどうしたら良いでしょうか?
- ユーザーがログインする前やログアウトした後に、ユーザーの状態に関係なく、クライアント自身に与えられた権限でアクセストークンで保護されたリソースサーバーを利用したい
- WebAPIやバッチなど、サービスプロセス自体で認証をかけてリソースサーバーを利用したい
このような場合、OAuth2のクライアントクレデンシャルグラントというグラントタイプを使って、クライアント認証によるアクセストークンを取得することができます。
今回は、KeycloakのJavaアダプターの一つであるKeycloak Spring Boot Adapterを使って、Keycloak Serverに対するクライアント認証を簡単に実装する方法を示します。
このようなケースの情報は少なく、最初は実装に苦労したので、誰かの参考になれば幸いです。
構成
手軽さを重視して、WindowsのローカルPC上に全ての構成を準備することにします。
役割 | アプリケーション |
---|---|
認可クライアント | Spring Boot 2.2.2.RELEASE Spring WebMVC/Security 5.2.2.RELEASE Keycloak Spring Boot Adapter 8.0.1 |
認可サーバー | Keycloak Server 8.0.1 |
認可リバースプロキシ | Keycloak Gatekeeper 8.0.1 |
バックエンドAPI/ネットワークキャプチャ | Fiddler Web Debugger v5.0.20194.41348 |
ブラウザ | Google Chrome 79.0.3945.79 |
認可クライアント/サーバー用JDK | OpenJDK 11 |
認可クライアント
今回のターゲットである、Spring BootによるRest APIです。
起動時にクライアントクレデンシャルグラントを使ったクライアント認証を認可サーバーへリクエストし、アクセストークンを取得します。
ブラウザからリクエストを受けると、アクセストークンを付加して認可リバースプロキシにリクエストし、バックエンドAPIからのレスポンスをそのまま出力します。
なお、アクセストークンには有効期限がありますが、クライアントクレデンシャルグラントではリフレッシュトークンは発行されないため、期限切れになる前にアクセストークンの再取得が必要になります。このために、クライアント認証のリクエストを定期的に行うようにスケジュールタスクを設けます。
認可サーバー
もちろん、Keycloak Serverを使います。
認可リバースプロキシ
昨年の UL Systems Advent Calendar 2018 の Keycloak GateKeeperの記事 でも取り上げた Keycloak GateKeeper を使って、バックエンドAPIをアクセストークンで保護します。
Keycloak GatekeeperはGo言語で実装されたアプリケーションで、APIの手前で OpenID Connect / OAuth2 に対応する認可リバースプロキシとして動作します。
ネットワークキャプチャ
HTTPリクエストとレスポンスを可視化するネットワークキャプチャとして、Fiddler Web Debugger(以下、Fiddler)を使います。
バックエンドAPI
FiddlerのAutoResponder機能を使って、リクエストに応じた固定のレスポンスを設定することにより、疑似的にバックエンドAPIの振る舞いをさせます。
認可クライアントの実装と起動
それでは、今回のターゲットである認可クライアントをSpring Bootをベースに実装します。
Spring Bootプロジェクトを生成する
最初に、Spring Initializr で以下の設定によりプロジェクトを生成します。
項目 | 設定 |
---|---|
Project | Gradle Project |
Language | Java |
Spring Boot | 2.2.2 |
Group | com.example |
Artifact | keycloakadapter |
Java | 11 |
Dependencies | Web, Security |
build.gradleにKeycloak Spring Bootアダプターを設定する
次に、build.gradleのdependenciesに以下の行を加えて、ビルドを行います。
implementation 'org.keycloak:keycloak-spring-boot-starter:8.0.1'
起動クラスにスケジュール利用を設定する
起動クラスであるKeycloakadapterApplicationに、@EnableScheduling
を設定します。
@EnableScheduling
@SpringBootApplication
public class KeycloakadapterApplication {
...
}
スケジュールタスクを実装する
(1) 認可サービスクライアント機能をセットアップする
認可クライアント機能で必要となる以下のクラスのインスタンスをセットアップします。
クラス名 | 説明 |
---|---|
KeycloakDeployment | KeycloakのエンドポイントURLやアクセス設定を保持する |
AuthzClient | Keycloakの認可サービスに対するクライアント機能を提供する |
KeycloakDeploymentは、Spring Bootプロパティを保持するKeycloakSpringBootPropertiesから生成できます。
AuthzClientの生成についてはインタフェースが提供されておらず一筋縄では行かないため、Keycloakアダプターに含まれているPolicyEnforcerという認可サービス利用時のクラスのコードを参考に実装します。
※Keycloakの認可サービスそのものは今回は使用しませんが、詳細については Keycloak 日本語ドキュメント - Authorization Services Guide を参照してください。
@Component
public class ClientAuthenticationTask {
private KeycloakDeployment deployment;
private AuthzClient authzClient;
private AccessToken token;
private String tokenString;
public ClientAuthenticationTask(KeycloakSpringBootProperties properties) {
this.deployment = KeycloakDeploymentBuilder.build(properties);
Configuration configuration = new Configuration(
properties.getAuthServerUrl(), properties.getRealm(), properties.getResource(), properties.getCredentials(),
this.deployment.getClient());
ClientAuthenticator authenticator = new ClientAuthenticator() {
@Override
public void configureClientCredentials(Map<String, List<String>> requestParams, Map<String, String> requestHeaders) {
Map<String, String> formparams = new HashMap<>();
ClientCredentialsProviderUtils.setClientCredentials(ClientAuthenticationTask.this.deployment, requestHeaders, formparams);
for (Entry<String, String> param : formparams.entrySet()) {
requestParams.put(param.getKey(), Arrays.asList(param.getValue()));
}
}
};
this.authzClient = AuthzClient.create(configuration, authenticator);
}
...
}
(2) 認可サービスクライアント機能を使ってアクセストークンを取得する
ClientAuthenticationTaskに、クライアント認証のリクエストを定期的に行うスケジュールタスクを@Scheduled
で指定し、fixedDelayStringでタスクの間隔をプロパティで指定するようにします。
上記の(1)でセットアップしたクライアント機能の呼び出しは、KeycloakアダプターのThrowables#retryAndWrapExceptionIfNecessary
とCallableを使って行います。
このあたりの実装も、Keycloakアダプターに含まれているAuthorizationResourceという認可サービス利用時のクラスのコードを参考に実装します。
AuthzClient#obtainAccessToken
が実際にクライアント認証のリクエストを発行しているメソッドで、これを使うことが今回の実装の主眼となっています。
クライアント機能のレスポンスからアクセストークンを取得し、AdapterTokenVerifier#verifyToken
を使って検証を行います。
@Scheduled(initialDelay = 0L, fixedDelayString = "${com.example.authentication.task.interval}")
public void authenticate() {
AuthorizationResponse response = null;
Callable<AuthorizationResponse> callable = new Callable<AuthorizationResponse>() {
@Override
public AuthorizationResponse call() throws Exception {
return new AuthorizationResponse(ClientAuthenticationTask.this.authzClient.obtainAccessToken(), false);
}
};
try {
response = callable.call();
} catch (Exception cause) {
response = Throwables.retryAndWrapExceptionIfNecessary(callable, null, "Failed to obtain access token", cause);
}
if (response == null) {
return;
}
try {
this.token = AdapterTokenVerifier.verifyToken(response.getToken(), this.deployment);
} catch (VerificationException e) {
return;
}
this.tokenString = response.getToken();
}
public AccessToken getToken() {
return this.token;
}
public String getTokenString() {
return this.tokenString;
}
クライアントのRest APIを実装する
上記のクライアント機能で取得したアクセストークンをHTTPヘッダに付加して、認可リバースプロキシのURLにリクエストします。
ここでは実装を示しませんが、クライアント機能が取得したAccessTokenからスコープやロール等の様々な情報を取得することもできます。
@RestController
public class ClientController {
@Autowired
private ClientAuthenticationTask task;
@GetMapping("/client")
public String client() {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(task.getTokenString());
ResponseEntity<String> response = new RestTemplate().exchange(
"http://localhost:3000/secured",
HttpMethod.GET,
new HttpEntity<String>(headers),
String.class);
return response.getBody();
}
}
Webセキュリティを設定する
Spring SecurityのWebSecurityConfigurerAdapterを継承して、全てのアクセスを許可するように設定します。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests().anyRequest().permitAll();
}
}
Keycloakの設定方法を指定する
KeycloakSpringBootConfigResolverのBean定義を行うことによって、KeycloakをSpring Bootプロパティで設定することとします。
@Configuration
public class KeycloakConfigResolverConfiguration {
@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
Spring Bootプロパティの設定を行う
KeycloakのレルムとサーバーURL、クライアントID、下記のKeycloak Server管理画面に表示されるクライアントシークレットを設定します。
また、上記のクライアント認証のスケジュールタスクの間隔に、アクセストークンの有効期間である5分よりも少し短い値として4分(PT4M)を指定します。
keycloak.realm = example
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.resource = client
keycloak.credentials.secret = 50c39441-e45d-46da-9069-2f04c3c0d3fa
com.example.authentication.task.interval = PT4M
server.port = 8000
これで認可クライアントの実装は完了です。
プロジェクトを起動する
順番が前後しますが、下記の認可サーバーの導入と設定が終わってから、認可クライアントのGradleプロジェクトを起動します。
C:\example\keycloakadapter>gradle run
認可サーバーの導入と設定
Keycloak Serverのダウンロードと起動
Keycloakのダウンロードページで、ServerのZIPをダウンロード、解凍します。
解凍したフォルダ直下のbin/standalone.bat を起動します。
C:\example\keycloak-8.0.1\bin>standalone.bat
起動後に http://localhost:8080/auth にアクセスし、『Administration Console』(以下、管理画面)から管理ユーザを作成してログイン後、以下の手順で日本語化しておきます。
- Realm Settings で masterレルムの『Themes』タブを開く
- 『Internationalization Enabled』をONにする。
- 『Default Locale』で『ja』を選択し、saveする。
- 右上の管理ユーザを選択し、『Sign out』で一度サインアウトする。
Keycloak Serverの設定
今回の構成のために、Keycloak Serverの管理画面で以下の設定を行います。
対象 | 項目 | 設定内容 |
---|---|---|
レルム | レルム名 | example |
ロール | ロール名 | CLIENT |
クライアント(1) | クライアントID | gatekeeper |
アクセスタイプ | confidential | |
有効なリダイレクト URI | http://localhost:3000/* | |
クライアント(2) | クライアントID | client |
アクセスタイプ | confidential | |
有効なリダイレクト URI | http://localhost:8000/* | |
サービスアカウントの有効 | オン | |
サービスアカウントロール | レルムロール=CLIENT |
Keycloak Serverにaudクレームの設定を追加する
昨年の Keycloak GateKeeperの記事 で取り上げた、アクセストークン内の"aud"クレームを自身のクライアントIDと一致していることを確認する問題について、執筆時点では未だに issueが解決していない ため、同様の回避設定を行います。
- Keycloakの管理画面で、clientの『マッパー』タブで、『作成』する。
- 名前を付けて、マッパータイプで『Audience』を選択する。
- 『Included Client Audience』で『gatekeeper』を選択する。
- 『アクセストークンに追加』のみONにして、『保存』する。
設定後はこのようになります。
認可リバースプロキシの導入と設定
これも昨年の Keycloak GateKeeperの記事 と同様に設定していきます。
Keycloak Gatekeeperのダウンロード
上記のKeycloakのダウンロードページから GatekeeperのWindows版をダウンロードします。
ダウンロードしたファイル keycloak-gatekeeper-windows-amd64.tar.gz を解凍して、実行ファイルである keycloak-gatekeeper.exe を配置します。
この配置したフォルダを、Keycloak Gatekeeperの実行フォルダとします。
Keycloak Gatekeeperの設定
Keycloak Gatekeeperの実行フォルダに、次の内容の設定ファイルconfig.yaml
を新規作成します。
/securedのURLに対してはCLIENTロールが必要としています。
listen: localhost:3000 #自身のポートを指定する
discovery-url: http://localhost:8080/auth/realms/example #Keycloak ServerのレルムのURL
client-id: gatekeeper #KeycloakのgatekeeperクライアントID
client-secret: 76238c91-0615-4825-8ec1-64c1439feda1 #Keycloak Serverのgatekeeperクライアントのシークレットを設定する
redirection-url: http://localhost:3000 #Keycloak Serverからリダイレクトで戻るエンドポイントURL
upstream-url: http://localhost:8888 #バックエンドAPIのURL
resources:
- uri: /secured
roles:
- CLIENT #/securedはCLIENTロールが必要
no-redirects: true # 認証が必要な場合にKeycloak Serverにリダイレクトしないようにする
enable-logging: true # ログ出力設定
verbose: true # デバッグログ出力設定
Keycloak Gatekeeperの起動
設定ができたら、設定ファイルを指定して起動します。
C:\example\keycloak-gatekeeper>keycloak-gatekeeper.exe --config config.yaml
バックエンドAPI/ネットワークキャプチャの導入と設定
Fiddlerのダウンロードと起動
Fiddlerのダウンロードページで、求められる項目を入力してWindows版のインストーラー FiddlerSetup.exe をダウンロードし、インストールを行います。
(参考:『HTTP通信のキャプチャをとるツールFiddlerをWindowsにインストールする』)
インストール後、Fiddlerを起動します。
Fiddlerは初期状態では、起動直後からローカルPC上のネットワークキャプチャを自動的に開始しますが、今回の構成で行うリクエスト以外は不要なので、以下の手順でlocalhostをホストとするリクエストのみを表示する設定を行います。
- Filtersタブを表示します。
- 『Use Filters』をONにします。
- 『Hosts』の2つ目のプルダウンから『Show only the following Hosts』を選択します。
- 上記プルダウンの直後の入力項目に『localhost』を入力します。
- 最後に『Actions』→『Run Filterset now』を押して設定を有効化します。
Fiddlerの設定
Fiddlerの 『AutoResponder』タブで『Add Rule』として、以下の内容のようにsecuredの疑似API設定を行います。
-
リクエスト(下段の『Request URL Pattern』に入力する)
EXACT:http://localhost:8888/secured
-
レスポンス(『Edit Response...』で『Raw』タブに入力する)
HTTP/1.1 200 OK Content-Type: application/json Content-Length: 21 {"message":"secured"}
最後に『Enable rules』をONにすると、AutoResponderが有効になります。
動作確認
Chromeで認可クライアントのURL http://localhost:8000/client にアクセスすると、バックエンドAPIのレスポンスが表示されます。
これによって、クライアント認証によるアクセストークンを使ったリクエストが成功していることが分かります。
ここでFiddlerでバックエンドAPIへのリクエストを見ると、ロールをはじめとしてアクセストークンが表していた情報がバックエンドAPIにHTTPヘッダで渡されていることが確認できます。
X-Auth-Audience: gatekeeper
X-Auth-Email:
X-Auth-Expiresin: 2019-12-16 16:03:58 +0000 UTC
X-Auth-Groups:
X-Auth-Roles: CLIENT
X-Auth-Subject: ae609fe7-80bd-461c-a5c3-3ce95e7321cc
X-Auth-Token: eyJ...
X-Auth-Userid: service-account-client
X-Auth-Username: service-account-client
おわりに
今回はクライアント認証のみの実装例を示しましたが、Webアプリではユーザーがログインして得られるアクセストークンと、クライアント認証によるアクセストークンを使い分けるといったユースケースが考えられます。
KeycloakのSpring Boot/Securityアダプターはまだまだ使いこなすと面白そうなので、何かあれば情報共有をお待ちしています!