CLOVER🍀

That was when it all began.

Spring Securityを使って、KeycloakでOpenID Connect

ちょっと前に、KeycloakのClient Adapterを使って、Keycloakに対してOpenID Connectを使って連携するエントリをいくつか
書いてみました。

今度は、KeycloakのClient Adapterは使わず、もう少し汎用に近いライブラリでKeycloakに対してOpenID Connectを使って
連携してみたいと思います。

対象は、Spring Securityです。

参考にするのは、すでに存在するこちらのブログエントリ。

Spring Boot 1/2のアプリにKeycloakのOpenID Connectを使ってシングルサインオン

あと、こちらのサンプルコードも。
https://github.com/spring-projects/spring-boot/tree/v2.0.4.RELEASE/spring-boot-samples/spring-boot-sample-oauth2-client

と、これを案内しただけでは終われないので、自分でも試してみたいと思います。

環境

このエントリで使った環境は、こちら。

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-0ubuntu0.18.04.1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

$ mvn -version
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T03:33:14+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-33-generic", arch: "amd64", family: "unix"

準備

Maven依存関係など、pom.xmlの主要な定義はこちら。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring-boot.version>2.0.4.RELEASE</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

Spring BootのWeb、SecurityのStarter、それからOAuth2のClientライブラリを加えているところがポイントです。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>

Spring Boot 2.0/Spring Security 5.0でOAuth2を使用するには、これらのライブラリを追加する必要があります。

OAuth 2.0 Client - spring-security-oauth2-client.jar

OAuth 2.0 JOSE - spring-security-oauth2-jose.jar

Keycloakのバージョンは、4.3.0.Finalです。

Keycloakの管理アカウントは、以下のコマンドで作成。

$ bin/add-user-keycloak.sh -u keycloak-admin -p password

## Keycloak Server自体を再起動
$ bin/add-user.sh -u admin -p password
$ bin/jboss-cli.sh -c -u=admin -p=password --command=reload

また、Keycloakには以下の情報でRealmおよびユーザーを作成しておきます。

  • Realm … demo-api
  • Client ID … sample-rest-api
  • テスト用のユーザー … api-user

ClientのCredentialsなどは、適宜取得します。…というか、Spring Securityの設定を行う時に、いろいろ出てきます。

サンプルコードを書く

それでは、サンプルコードを書いていきます。

Spring Securityの設定は、こちらで。
src/main/java/org/littlewings/keycloak/spring/SecurityConfig.java

package org.littlewings.keycloak.spring;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Value("${keycloak.logout-uri}")
    String logoutUri;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/secure/**").authenticated()
                .anyRequest().permitAll()
                .and()
                .oauth2Login()
                .and()
                .logout()
                .logoutSuccessUrl("/public/hello")
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));  // enable GET access
    }
}

認証が必要なページにはOAuth2 Loginを行うようにし、その範囲は「/secure」配下としました。また、それ以外はアクセスフリーです。

設定は、このあたりを参考に。

OAuth 2.0 Login

OAuth 2.0 Login / Provide a WebSecurityConfigurerAdapter

ログアウト後はログイン不要なURLに戻ってくるようにして、ログアウト用のURLは今回はGETアクセスを許容(CSRF対策オフ)。

Logging Out

続いて、Controller。

未ログインでも、アクセス可能なControllerを作成。
src/main/java/org/littlewings/keycloak/spring/controller/PublicController.java

package org.littlewings.keycloak.spring.controller;

import java.util.Collections;

import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("public")
public class PublicController {
    @GetMapping("hello")
    public String hello() {
        return "Hello Application!!";
    }

    @GetMapping("user")
    public Object user(OAuth2AuthenticationToken authenticationToken) {
        return authenticationToken != null ? authenticationToken : Collections.emptyMap();
    }
}

ログイン必須ではないControllerでも、ログインしていればユーザーの情報が取得できるようです。ここでは、ログインしているユーザーの情報は、
OAuth2AuthenticationTokenを使って受け取ります。

未ログインの時はOAuth2AuthenticationTokenは渡されない(nullになる)ので、そこは考慮の上ということで。

設定上、ログイン必須にしているControllerは、こちら。
src/main/java/org/littlewings/keycloak/spring/controller/SecureController.java

package org.littlewings.keycloak.spring.controller;

import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("secure")
public class SecureController {
    @GetMapping("hello")
    public String hello() {
        return "Hello Secure Application!!";
    }

    @GetMapping("user")
    public Object user(OAuth2AuthenticationToken authenticationToken) {
        return authenticationToken;
    }
}

こちらでのOAuth2AuthenticationTokenの受け取りは、ログインが前提になっているので特にnullの確認は行わずそのままレスポンスとします。

起動クラスは、こちら。
src/main/java/org/littlewings/keycloak/spring/App.java

package org.littlewings.keycloak.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

ソースコードは、こんな感じです。

設定

割と重要になる、設定ファイルです。
src/main/resources/application.properties

spring.security.oauth2.client.registration.keycloak.client-id=sample-rest-api
spring.security.oauth2.client.registration.keycloak.client-secret=19d00092-8897-41c0-ac41-1847151797b4
spring.security.oauth2.client.registration.keycloak.client-name=keycloak client example
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.redirect-uri-template={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.keycloak.client-authentication-method=basic
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code

spring.security.oauth2.client.provider.keycloak.authorization-uri=http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak.token-uri=http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/token
spring.security.oauth2.client.provider.keycloak.user-info-uri=http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/userinfo
spring.security.oauth2.client.provider.keycloak.jwk-set-uri=http://172.17.0.2:8080/auth/realms/demo-api/protocol/openid-connect/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

spring.jackson.serialization.indent_output=true

最後の以下の部分は、ControllerのレスポンスとなるJSONをフォーマットするためのものですね。

spring.jackson.serialization.indent_output=true

「spring.security.oauth2.client.registration.〜」および「spring.security.oauth2.client.provider.〜」に関する説明は、以下を参照します。

OAuth 2.0 Login / Setting the redirect URI

OAuth 2.0 Login / ClientRegistration

OAuth 2.0 Login / Spring Boot 2.0 Property Mappings

OAuth2 / Client


「spring.security.oauth2.client.registration.[registrationId].〜」については、好きなregistrationIdを指定して設定します。

  • client-id … Keycloakで登録したClient ID
  • client-secret … Keycloakから取得したClientのSecret
  • client-name … 任意の値(ログイン先の選択時に表示)
  • provider … 次の「spring.security.oauth2.client.provider.[provider].〜」で使うproviderの名前
  • scope … openid、email、profileから選択
  • redirect-uri-template … ログイン後に、認証サーバー(Keycloak)からリダイレクトするURL。通常、「{baseUrl}/login/oauth2/code/{registrationId}」でOK
  • client-authentication-method … basicかpostを指定
  • authorization-grant-type … OAuth 2.0でのAutorization Grantを指定。authorization_codeまたはimplicit

そして、「spring.security.oauth2.client.provider.[provider].〜」ではKeycloakへのエンドポイントの設定を行っていきます。

各種URLは、以下を見て設定していきます。

Other OpenID Connect Libraries

KeycloakのClient Adapterを使わない場合に設定する、各種エンドポイントの設定です。

Endpoints

  • authorization-uri … Authorization Endpointを指定
  • token-uri … Token Endpointを指定
  • user-info-uri … Userinfo Endpointを指定
  • jwk-set-uri … Certificate Endpointを指定
  • user-name-attribute … Userinfo Responseの内容のうち、ユーザーの名前に該当する属性を指定。今回は「preferred_username」を設定

これで準備完了です。

確認

パッケージングして

$ mvn package

起動。

$ java -jar target/spring-security-example-0.0.1-SNAPSHOT.jar

書いてから気づいた、名前が…。

ログイン不要なURLにアクセス。

http://localhost:8080/public/hello

http://localhost:8080/public/user

ユーザー情報を表示する方は、まだなにも出ません。

次に、ログインが必要なページにアクセス。

http://localhost:8080/secure/hello

すると、認証先の選択画面が現れます。といっても、今回はKeycloak用に作成したもののみですが。

リンク先に行くと、Keycloakのログイン画面が表示されるので、ここでログイン。

リダイレクトして戻ってくると、今度は表示できます。

ユーザー情報も表示できます。
http://localhost:8080/secure/user

また、ログイン不要なページの方でもユーザー情報が取得できるようになっています。
http://localhost:8080/public/user

最後、ログアウト。
http://localhost:8080/logout

一瞬でログアウトして、ログイン不要のページに戻ってきます。

再度ログインが必要なページにアクセスすると、またこちらのページが表示されます。

この状態だとKeycloak自体からはログアウトしていないので、リンク先に行くとすぐにログインが必要なページが表示されます。

Keycloak自体も合わせてログアウトしたい場合は、ログアウト用のエンドポイントを呼び出すように組み込むんでしょうねぇ…。

Endpoints

まとめ

KeycloakのClient Adapterではなく、Spring SecurityだけでKeycloakを使ったOpenID Connectをしてみました。こちらの方がエンドポイントとかの意識が必要で、
少し詳細に踏み込めた感があります。

が、もうちょっとプロトコル自体を勉強しないとなぁ、という気になりました…。