memo:Keycloak Spring Boot Adapter
はじめに
KeycloakのSpring Boot Adapterを使ってOpenID Connectを実装したメモ
OpenID Connect
- OpenID Connect Core 1.0 日本語訳
- OpenID Connect Basic Client Implementer's Guide 1.0 日本語訳
- OpenID Connect Discovery 1.0
Keycloak
- Keycloak - Securing Applications and Services Guide
- Keycloak - Securing Applications and Services Guide(日本語)
1. 環境
Spring Bootのプロジェクトで KeycloakのSpring Boot Adapter
を使う
-
macOS Big Sur 11.3.1
-
Docker Desktop 3.5.1
-
Keycloak 15.0.2
-
サンプルPG : Kotlin
- Source : GitHub
2. 準備
Keycloak
インストール
docker-compose.yml
version: '3'
services:
keycloak:
image: jboss/keycloak
container_name: oidc-keycloak-spring-boot-starter
ports:
- 8081:8080
environment:
- KEYCLOAK_USER=admin
- KEYCLOAK_PASSWORD=password
Dockerイメージ作成
docker-compose build
- Image
jboss/keycloak
が作成されてることを確認
% docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
jboss/keycloak latest 4ed3cf7f13a5 7 weeks ago 714MB
コンテナ起動
docker-compose -p oidc-keycloak-spring-boot-starter up -d
- Container
oidc-keycloak-spring-boot-starter
が作成されて起動していることを確認
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4f6a9179e655 jboss/keycloak "/opt/jboss/tools/do…" 5 minutes ago Up 5 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 8443/tcp oidc-keycloak-spring-boot-starter
設定
Keycloak設定画面にログイン
ブラウザから接続
http://localhost:8081/auth/
Keycloakにログイン
admin / password
Client作成
- Master - Configure - Clients -
Create
- Add Client
- Client ID :
spring-boot-app
- Client Protocol :
openid-connect
- Root URL : 空
- Client ID :
- Add Client
- Master - Configure - Clients -
Spring-boot-app
User作成
-
Master - Manage - Users -
Add user
- Username :
hoge
- Email :
hoge@gebogebo.com
- First Name :
Ho
- Last Name :
Ge
- Username :
-
Master - Manage - Users -
hoge
- Credentialsタブ
- Password : 任意のパスワード
- Temporary :
OFF
Set Password
- Credentialsタブ
Spring boot
プロジェクト作成
- Project : Gradle Project
- Language : Kotlin
- Dependencies
- Spring Web
- Thymeleaf
画面作成
index.html
- トップページ、ログイン前
- ボタンを押したらログインする
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h3>OpenID Connect Login Test</h3>
<button onclick="location.href='./secure/welcome'">
Login with Keycloak
</button>
</body>
</html>
welcome.html
- ログイン(Keycloak認証)後に表示する
- ログインしたユーザーの情報を表示する
- Logoutボタンでログアウトする
- Settingで設定画面に遷移する
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Welcome</title>
</head>
<body>
<h3>Welcome</h3>
<div th:text="|user name = ${userinfo.username}|"></div>
<div th:text="|email = ${userinfo.email}|"></div>
<div th:text="|last name = ${userinfo.lastname}|"></div>
<div th:text="|first name = ${userinfo.firstname}|"></div>
<div th:text="|id token.issuer = ${userinfo.idTokenInfo.issuer}|"></div>
<div th:text="|id token.audience = ${userinfo.idTokenInfo.audience}|"></div>
<div th:text="|access token.issuer = ${userinfo.accessTokenInfo.issuer}|"></div>
<div th:text="|access token.audience = ${userinfo.accessTokenInfo.audience}|"></div>
<div th:text="|access token.roles = ${userinfo.accessTokenInfo.roles}|"></div>
<div th:text="|access token.scopes = ${userinfo.accessTokenInfo.scopes}|"></div>
<div>
<button onclick="location.href='./logout'">
Logout
</button>
</div>
<div>
<button onclick="location.href='./setting'">
Setting
</button>
</div>
</body>
</html>
setting.html
- 設定画面
- Admin専用
- openid-configurationデータを表示する
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Setting</title>
</head>
<body>
<h3>Admin Only</h3>
<div th:text="|openid-configuration|"></div>
<div th:text="|${openidconfiguration}|"></div>
<button type="button" onclick="history.back()">
Back
</button>
</body>
</html>
コントローラ作成
AppController
@Controller
class AppController {
@GetMapping("/")
fun index():String{
return "index"
}
@GetMapping("/secure/welcome")
fun welcome(model: Model):String{
model.addAttribute("userinfo", UserInfoService().getUserInfo())
return "welcome"
}
@GetMapping("/secure/logout")
fun logout(request: HttpServletRequest):String{
request.logout()
return "redirect:/"
}
@GetMapping("/secure/setting")
fun setting(model: Model):String{
model.addAttribute("openidconfiguration", "No data")
return "setting"
}
}
UserInfoService
class UserInfoService {
fun getUserInfo():UserInfo{
return UserInfo()
}
class UserInfo {
var username = "No data"
var emailID = "No data"
var lastname = "No data"
var firstname = "No data"
var idTokenInfo = IdTokenInfo()
var accessTokenInfo = AccessTokenInfo()
}
class IdTokenInfo {
var issuer = "No data"
var audience = "No data"
}
class AccessTokenInfo {
var issuer = "No data"
var audience = "No data"
var roles = "No data"
var scopes = "No data"
}
}
この時点で一旦一連の動作確認
-
http://localhost:8080
で index.html を表示する -
Login with Keycloak
で welcome.html に遷移(認証は無い)- UserInfoはすべて No data と表示
-
Setting
で setting.html を表示- openid-configuration は No data と表示
-
back
で戻る
-
Logout
最初の画面に戻る
3. Spring Boot Adapterの導入
実装
build.gradle.kts
...
dependencies {
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security") <- 追加
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.keycloak:keycloak-spring-boot-starter:15.0.2") <- 追加
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
...
-
keycloak-spring-boot-starterのバージョンは latest の値 を使う
-
修正したら画面の[Gradle]から更新ボタンを押さないと反映されてないので注意
SecurityConfig[4]
@KeycloakConfiguration
class SecurityConfig : KeycloakWebSecurityConfigurerAdapter() {
@Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
val keycloakAuthenticationProvider = keycloakAuthenticationProvider()
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(SimpleAuthorityMapper())
auth.authenticationProvider(keycloakAuthenticationProvider)
}
@Bean
override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy {
return RegisterSessionAuthenticationStrategy(SessionRegistryImpl())
}
@Bean
fun keycloakConfigResolver() : KeycloakConfigResolver {
return KeycloakSpringBootConfigResolver()
}
override fun configure(http: HttpSecurity) {
super.configure(http)
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/secure/setting").hasRole("admin")
.anyRequest().authenticated()
}
@Autowired
lateinit var keycloakClientRequestFactory: KeycloakClientRequestFactory
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
fun keycloakRestTemplate(): KeycloakRestTemplate {
return KeycloakRestTemplate(keycloakClientRequestFactory)
}
}
- configureGlobal()
- configure()
-
permitAll()
→/
は認証不要-
hasRole("admin")
→/secure/setting
は admin だけがアクセス可能
-
-
anyRequest().authenticated()
→それ以外 は 認証 が必要 - keycloakRestTemplate()
- Keycloakのエンドポイントに個別にアクセスする際に便利な
KeycloakRestTemplate
のBean[7]
- Keycloakのエンドポイントに個別にアクセスする際に便利な
UserInfoService.getUserInfo
fun getUserInfo():UserInfo{
val authentication = SecurityContextHolder.getContext().authentication
val principal = authentication.principal as KeycloakPrincipal<*>
val session = principal.keycloakSecurityContext
val accessToken = session.token
val idToken = session.idToken
val info = UserInfo()
info.username = idToken.preferredUsername?:""
info.email = idToken.email?:""
info.lastname = idToken.familyName?:""
info.firstname = idToken.givenName?:""
info.idTokenInfo.issuer = idToken.issuer?:""
info.idTokenInfo.audience = idToken.audience?.joinToString(",")?:""
info.accessTokenInfo.issuer = accessToken.issuer?:""
info.accessTokenInfo.audience = accessToken.audience?.joinToString(",")?:""
val realmAccess = accessToken.realmAccess
info.accessTokenInfo.roles = realmAccess.roles?.toString()?:""
info.accessTokenInfo.scopes = accessToken.scope?:""
return info
}
setting
@Controller
class AppController {
...
@GetMapping("/secure/setting")
fun setting(model: Model):String{
val response = template.getForEntity(endpoint, String::class.java)
model.addAttribute("openidconfiguration", response.body)
return "setting"
}
@Autowired
private lateinit var template: KeycloakRestTemplate
private val endpoint = "http://localhost:8081/auth/realms/master/.well-known/openid-configuration"
}
application.properties
keycloak.enabled=true
keycloak.auth-server-url=http://localhost:8081/auth
keycloak.realm=master
keycloak.public-client=false
keycloak.resource=spring-boot-app
keycloak.credentials.secret=daa4d455-68d6-4724-9ce5-3a76f5bda0d1
動作確認
Login with Keycloak
OpenID Connect の Authorization Code Flow でログインし、トークンに含まれるユーザー情報を表示する
Source | Destination | ||
---|---|---|---|
Login with Keycloak をクリック |
|||
→ | Browser | App | GET /secure/welcome HTTP/1.1 |
Adapter が Intercept | |||
← | Adapter | Browser | HTTP/1.1 302 |
→ | Browser | Adapter | GET /sso/login HTTP/1.1 |
← | Adapter | Browser | HTTP/1.1 302 |
→ | Browser | Keycloak | GET /auth/realms/master/protocol/openid-connect/auth ?response_type=code &client_id=spring-boot-app &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fsso%2Flogin &state=59056b4f-0599-462f-9165-56f2e0df4bb5 &login=true &scope=openid HTTP/1.1 |
← | Keycloak | Browser | HTTP/1.1 200 OK (text/html) |
認証画面を表示 User/Passwordを入力してSubmit |
|||
→ | Browser | Keycloak | POST /auth/realms/master/login-actions/authenticate ?session_code=RBWjMrRlRtS1OdJrrnqPt7WezK4KEk32gBJC7oiPKFE&execution=50b110a9-fbd3-4853-a240-ca5b0b60cca4 &client_id=spring-boot-app &tab_id=hhOKRt1DJm8 HTTP/1.1 (application/x-www-form-urlencoded) |
← | Keycloak | Browser | HTTP/1.1 302 Found |
→ | Browser | Adaptor | GET /sso/login ?state=59056b4f-0599-462f-9165-56f2e0df4bb5 &session_state=8fd4668f-8793-4237-98cf-a4b29d10f1ef &code=3c82ed51-9f94-43d0-9753-c67a6c93bc28.8fd4668f-8793-4237-98cf-a4b29d10f1ef.abeaef51-8f0e-468a-aaf2-d17dca021dbr7 HTTP/1.1 |
→ | Adaptor | Keycloak | POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1 (application/x-www-form-urlencoded) |
← | Keycloak | Adaptor | HTTP/1.1 200 OK , JavaScript Object Notation (application/json) |
← | Adaptor | Browser | HTTP/1.1 302 |
→ | Browser | App | GET /secure/welcome HTTP/1.1 |
AppController.welcome() | |||
← | App | Browser | HTTP/1.1 200 (text/html) |
welcome.htmlを表示 |
Access Token期限切れ時の再取得
- アクセストークンが切れた頃にログインし直すとリフレッシュトークンを使ってアクセストークンを再取得する
- アクセストークンの有効期限は1分
- リフレッシュトークンの有効期限は30分
Source | Destination | ||
---|---|---|---|
Login with Keycloak をクリック |
|||
→ | Browser | App | GET /secure/welcome HTTP/1.1 |
Adapter が Intercept | |||
→ | Adapter | Keycloak | POST /auth/realms/master/protocol/openid-connect/token HTTP/1.1 (application/x-www-form-urlencoded) |
← | Keycloak | Adapter | HTTP/1.1 200 OK , JavaScript Object Notation (application/json) |
← | Adapter | Browser | HTTP/1.1 200 (text/html) |
Logout
Source | Destination | ||
---|---|---|---|
Logout をクリック |
|||
→ | Browser | App | GET /secure/logout HTTP/1.1 |
AppController.logout() - request.logout() |
|||
→ | Adapter | Keycloak | POST /auth/realms/master/protocol/openid-connect/logout HTTP/1.1 (application/x-www-form-urlencoded) |
← | Keycloak | Adapter | HTTP/1.1 204 No Content |
AppController.logout() - return "redirect:/" |
|||
← | Adapter | Browser | HTTP/1.1 302 |
→ | Browser | App | GET / HTTP/1.1 |
AppController.index() | |||
← | App | Browser | HTTP/1.1 200 (text/html) |
openid-configuration
-
KeycloakRestTemplate
を使ってopenid-configuration にアクセスする
Source | Destination | ||
---|---|---|---|
setting をクリック |
|||
→ | Browser | App | GET /secure/setting HTTP/1.1 |
AppController.setting() template.getForEntity() |
|||
→ | App | Keycloak | GET /auth/realms/master/.well-known/openid-configuration HTTP/1.1 |
← | Keycloak | App | HTTP/1.1 200 OK , JavaScript Object Notation (application/json) |
← | App | Browser | HTTP/1.1 200 (text/html) |
openid-configuration(JSON)を画面に表示 |
-
Access Type : confidential
OAuth ClientType
http://openid-foundation-japan.github.io/rfc6749.ja.html#client-types
https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 ↩︎ -
Valid Redirect URIs
https://keycloak-documentation.openstandia.jp/3.4/ja_JP/server_admin/index.html ↩︎ -
Secret
https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401#14-client_secret_basic
https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication ↩︎ -
https://www.baeldung.com/spring-boot-keycloak#securityconfig ↩︎
-
https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_security_adapter
https://keycloak-documentation.openstandia.jp/master/ja_JP/securing_apps/index.html#_spring_security_adapter ↩︎ -
https://www.keycloak.org/docs/latest/securing_apps/index.html#naming-security-roles ↩︎
-
https://www.keycloak.org/docs/latest/securing_apps/index.html#client-to-client-support ↩︎
-
(このフローの中で) ID Token を Client と Token Endpoint の間の直接通信により受け取ったならば, トークンの署名確認の代わりに TLS Server の確認を issuer の確認のために利用してもよい (MAY).
http://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html#IDTokenValidation ↩︎
Discussion