SpringSecurityによるLINEログインをローカル環境だけで動作させる
以前、SpringBoot及びSpringSecurityを使ってLINEログインを実現方法についての記事を書きました。

SpringBootで構築したWebアプリに対して、SpringSecurityによるLINEログインを実装した際の学び、及び備忘録です。 ※SpringBootに関しては簡単に解説している部分もありますが、最低限の触れ
https://blog.techno-core.jp/posts/line-login-by-spring-securitySSOでログインを実装するとユーザー観点では余計なアカウントの管理がなくなって便利ですが、開発者としてはローカル環境だけで開発を完結させたり、ログイン周りのテストを自動化することが難しくなります。実際のサービスには接続しない状態で、ローカル環境だけでSSOが実現できるようになると便利です。
この記事では、SpringSecurityを使って実装したLINEログインをローカル環境だけで動作させるようにするための構築方法を紹介します。
LINEログインだけに限らず、OIDCを使ったSSOをローカル環境だけで構築してテストを自動化したりCIに組み込む際にも応用可能ですので、参考になれば幸いです。
環境
使用するSpringBootのプロジェクトは前回の記事で作成したものをそのまま使用します。
手を動かしながら試したい場合は、前回の記事を参照してSpringBootのプロジェクトを用意してLINEログインを実装しておいてください。
今回は、SpringBootのWebアプリに加えて以下のツールを使用します。
- Wiremock
- Docker
Wiremockについての詳細な説明は割愛しますが、ざっくり説明するとAPIのモックを作成することができるツールで、外部サービスを使用するWebアプリのテストする際に、外部サービスに見立てたモックが簡単に構築できるJavaベースのフレームワークです。

WireMock is a tool for building mock APIs. API mocking enables you build stable, predictable development environments when the APIs you depend on are unreliable or don’t exist.
https://wiremock.org/WiremockはJVM上で動作しますが、今回はDockerイメージを使用するので、別途インストールする必要はありません。
Dockerをインストールしてdocker
コマンドが使える状態にしておいてください。
構築の流れ
ローカルでLINEログインを実現できるようになるまでの流れは以下になります。
- Wiremockのレスポンスを拡張するjarファイルの作成
- Dockerイメージの作成
- mappingsファイルの作成
- コンテナの起動
作業に入る前に、Wiremockのスタブについて簡単に整理しておきます。
Wiremockによるスタブの作成
Wiremockを使うと、特定のHTTPリクエストが送られてきた際に、あらかじめ決められたレスポンスを返すようにスタブを作成することができます。以下のようなjsonファイルをmappings
ディレクトリ配下においておくことで、サーバー起動時に自動的にスタブとして設定してくれます。
{
"request": {
"urlPathTemplate": "/contacts/{contactId}/addresses/{addressId}"
"method" : "GET"
},
"response" : {
"status" : 200
}
}

WireMock supports matching of requests to stubs and verification queries using the following attributes.
https://wiremock.org/docs/request-matching/この例ではステータスコード200を返しているだけですが、レスポンスヘッダやレスポンスボディも設定することができます。
しかし、このようなスタブでは静的な値を返すことしかできません。ですが、実際のWebアプリケーションを想定した場合、リクエストの内容に応じて動的な値をレスポンスしたい場合もあるでしょう。
そのような場合、WiremockではTransforming Responsesという仕組みが用意されており、ResponseDefinitionTransformerV2
インターフェースを実装したクラスを定義し、transform
メソッドをオーバーライドすることで、レスポンスをカスタマイズすることができます。
public static class ExampleTransformer implements ResponseDefinitionTransformerV2 {
@Override
public ResponseDefinition transform(ServeEvent serveEvent) {
return new ResponseDefinitionBuilder()
.withHeader("MyHeader", "Transformed")
.withStatus(200)
.withBody("Transformed body")
.build();
}
@Override
public String getName() {
return "example";
}
}
}
作成したTransformerを使用してレスポンスを返す場合は、スタブで使用したいTransformerを指定します。
{
"request": {
"method": "GET",
"url": "/local-transform"
},
"response": {
"status": 200,
"body": "Original body",
"transformers": ["my-transformer", "other-transformer"]
}
}

Transforming response definitions and responses via extensions
https://wiremock.org/docs/extensibility/transforming-responses/LINEログインなどのOIDCでは、セキュリティ対策としてリクエストにstate
やnonce
といったランダム文字列を生成し、リクエストに含めます(参考)。そして、プロバイダー(LINE)側は、nonce
の値を使って署名したIDトークンをレスポンスとして返します。
SpringSecurityを使ってOIDCを実装する場合、state
やnonce
の値は毎回フレームワークが自動でランダムに生成します。そのため、Wiremockを用いてランダムに生成された値を元にしたIDトークンを動的にレスポンスするには、Transformerを使う必要があります。
Wiremockのサーバーを起動させる方法は、大きく2つあります。
1つは、Wiremockを組み込みで使用する場合(MavenやGradleに依存関係を追加して、アプリからMockサーバーを起動する方式)で、もう一つはDockerイメージから起動する方法です。
Wiremockを組み込みで起動する方式であれば、単にTransformerのクラスを作成すればよいですが、WiremockをDockerコンテナ上で動作させる場合、拡張したTransformerを使うにはビルドしたjarファイルをコンテナに含めなければなりません。そのため、Dockerを使用する場合の方が手間がかかるのですが、CIへの組み込みや諸々の使い勝手の良さを考えると、Dockerを使ってスタンドアロンでWiremockを起動させる方が汎用性が高いので、今回はDockerに対応した方法を取ります。
Wiremockのレスポンスを拡張するjarファイルの作成
Transfomer用のクラスのjarファイルを作成するために、Webアプリとは別で独立したJava、またはKotlinのモジュールを作成します。ここではKotlinでMavenプロジェクトを作成することにします。プロジェクト名は任意ですが、ここではwiremock-extension
とします。
まずはpom.xmlを作成します。
ポイントは、Wiremockの拡張jarはJavaのバージョン11でコンパイルする必要があったので、pluginでバージョンを指定。
また、kotlinでコードを書く場合は、kotlinの標準ライブラリもWiremock側でjarとして読み込む必要があるため、そのための設定をmaven-shade-plugin
として追加しています。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>wiremock-extension</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
</properties>
<repositories>
<repository>
<id>mavenCentral</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
</repositories>
<build>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>2.1.20</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<mainClass>MainKt</mainClass>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<shadedArtifactAttached>true</shadedArtifactAttached>
<shadedClassifierName>all</shadedClassifierName>
<filters>
<filter>
<artifact>*:*</artifact>
<!-- 署名ファイルは除外 -->
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
<version>2.1.20</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>2.1.20</version>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>3.10.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>10.5</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20241224</version>
</dependency>
</dependencies>
</project>
次に、Transformerのクラスを作成します。
Transformer.kt
package org.example
import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder
import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformerV2
import com.github.tomakehurst.wiremock.http.ResponseDefinition
import com.github.tomakehurst.wiremock.stubbing.ServeEvent
import com.nimbusds.jose.JOSEObjectType
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.crypto.MACSigner
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import java.util.Date
class IdTokenTransformer(): ResponseDefinitionTransformerV2 {
override fun getName(): String = "id-token-transformer"
override fun transform(serveEvent: ServeEvent): ResponseDefinition? {
try {
val form = serveEvent.request.bodyAsString // "grant_type=authorization_code&code=....&..."
val code = form.split("&").firstOrNull { it.startsWith("code=") }?.substringAfter("=") ?: ""
val issuer = env("OIDC_ISSUER") ?: "http://127.0.0.1:8081"
val clientId = env("OIDC_CLIENT_ID") ?: "1234567890"
val secret = (env("OIDC_CLIENT_SECRET") ?: "12345678901234567890123456789012").toByteArray()
val kid = env("OIDC_JWK_KID") ?: "test-kid-1"
// --- JWT Claims ---
val now = Date()
val exp = Date(now.time + 3600_000) // 1h
val claims = JWTClaimsSet.Builder()
.issuer(issuer)
.audience(clientId)
.subject("sub-12345")
.issueTime(now)
.expirationTime(exp)
.claim("nonce", code) // nonce
.claim("email", "test@example.com")
.claim("name", "Test User")
.build()
// --- Header: HS256 ---
val headerB = JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JWT)
if (kid.isNotBlank()) headerB.keyID(kid)
val header = headerB.build()
// --- Sign with HMAC (HS256) ---
val signer = MACSigner(secret)
val jwt = SignedJWT(header, claims).apply { sign(signer) }.serialize()
// OAuth2 Token Response
val json =
"""{"access_token":"at-123","token_type":"Bearer","expires_in":3600,"id_token":"$jwt"}"""
return ResponseDefinitionBuilder()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(json)
.build()
} catch (e: Exception) {
return ResponseDefinitionBuilder()
.withStatus(500)
.withHeader("Content-Type", "application/json")
.withBody("""{"error":"hs256-transformer-failed","detail":"${e.message}"}""")
.build()
}
}
override fun applyGlobally(): Boolean = false
private fun env(name: String): String? =
System.getenv(name)?.takeIf { it.isNotBlank() }
}
作成するクラスはこれだけです。
ポイントはcode
の変数の値です。リクエストが送られてきた際にSpringSecurity側で生成されたランダムな値がcode
パラメータとして送られてくるので、その値を取得しています。
署名されたITトークンを作成するにあたり、cliendIdやclientSecretなどが必要になりますが、これらの値は環境変数から取得できるようにしておきます。
WebアプリによるLINEログインでは、署名のアルゴリズムとしてHS256が使用されているので(前回の記事を参照)、HS256のアルゴリズムを使用して、ITトークンを生成してレスポンスします。
クラスの定義が終わったら、mvn
コマンドを使ってビルドします。
mvn package -DskipTests
target
ディレクトリが作成され、wiremock-extension-1.0-SNAPSHOT.jar
とwiremock-extension-1.0-SNAPSHOT-all.jar
の2つのjarファイルが作成されていればOKです。
Dockerイメージの作成
今回作成したjarファイルを含んだWiremockのDockerイメージを作成するため、プロジェクト内にDockerfileを作成します。
内容は以下になります。コピー元のパスはDockerfileを作成した場所に応じて適宜変更してください。
FROM wiremock/wiremock:latest
COPY target/wiremock-extension-1.0-SNAPSHOT.jar /var/wiremock/extensions/id-token-transformer.jar
COPY target/wiremock-extension-1.0-SNAPSHOT-all.jar /var/wiremock/extensions/wiremock-extension-1.0.0-all.jar
Dcokerfileのあるディレクトリで、以下のコマンドでDockerイメージを作成します。
docker build -t wiremock-extension-oidc .
最終的には以下のディレクトリ構成になっている想定です。
wiremock-extension: プロジェクトルート
|-src
|-main
|-kotlin
|- org
|-example
|- IdTokenTransformer.kt
|-target
|- wiremock-extension-1.0-SNAPSHOT.jar
|- wiremock-extension-1.0-SNAPSHOT-all.jar
|-pom.xml
|-Dockerfile
Dockerイメージまで作成できれば、このMavenプロジェクトの役割はここで終わりです。
mappingsの作成
続いては、Wiremockのスタブを作成します。
ここではLINEログインを実装しているアプリ側のプロジェクトに作成することにします。プロジェクトのディレクトリの配下に、wiremock/mappings
ディレクトリを作成します。
その中に、必要なjsonファイルを作成します。
ここでは、4つのjsonファイルを作成します。
line-login-sample
|- src
|- wiremock
|- mappings
|- authorize.json
|- discovery.json
|- token.json
|- userinfo.json
まず1つ目は、discovery.json。
これは、issuerのURLを元に、エンドポイントの情報などのレスポンスを返すスタブです。issur_url/.well-known/openid-configuration
にリクエストが送られてきた場合のスタブになります。
discovery.json
{
"request": { "method": "GET", "url": "/.well-known/openid-configuration" },
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"issuer": "http://127.0.0.1:8081",
"authorization_endpoint": "http://127.0.0.1:8081/authorize",
"token_endpoint": "http://127.0.0.1:8081/token",
"revocation_endpoint": "http://127.0.0.1:8081/revoke",
"userinfo_endpoint": "http://127.0.0.1:8081/userinfo",
"scopes_supported": ["openid", "profile", "email"],
"jwks_uri": "http://127.0.0.1:8081/jwks",
"response_types_supported": ["code"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["ES256"],
"code_challenge_methods_supported": ["S256"]
}
}
}
2つ目はauthorize.json。
これは、/authorize
のリクエストに対するスタブで、LINEログインボタンを押したときに呼ばれます。ここで、nonce
とstate
はリクエストとして送られてきた値をそのまま使って、リダイレクトURLを作ってリダイレクトさせます。
authorize.json
{
"request": {
"method": "GET",
"urlPath": "/authorize",
"queryParameters": {
"response_type": {
"equalTo": "code"
},
"client_id": {
"matches": ".*"
},
"redirect_uri": {
"matches": ".*"
},
"scope": {
"contains": "openid"
},
"state": {
"matches": ".+"
}
}
},
"response": {
"status": 302,
"headers": {
"Location": "http://127.0.0.1:8080/login/oauth2/code/line?code={{request.query.nonce}}&state={{request.query.state}}"
},
"transformers": ["response-template"]
}
}
3つ目は、/token
のリクエストが呼ばれたときのスタブ。ここで、先に作っておいたTransformerの名前を指定する。これにより、IDトークンが返されるようになります。
token.json
{
"request": { "method": "POST", "url": "/token" },
"response": {
"transformers": ["id-token-transformer"]
}
}
4つ目は、認証が完了した後に、ユーザー情報を取得する際のスタブ。ここは必要に応じて情報を追加したり編集したりしてください。
userinfo.json
{
"request": { "method": "GET", "url": "/userinfo" },
"response": {
"status": 200,
"headers": { "Content-Type": "application/json" },
"jsonBody": {
"sub":"sub-12345",
"name":"Test User",
"email":"test@example.com"
}
}
}
コンテナの起動
最後に、docker-compose.yaml
を作成してコンテナを起動させます。先に作成しておいたwiremock-extension-oidc
のDockerイメージを指定します。また、commandで、--extensions
を指定し、Transformerとして使用するクラスを指定します。
環境変数の値のCLIENT_IDやCLIENT_SECRETは、アプリ側の設定値と合わせるようにしてください。
docker-compose.yaml
version: '3.8'
services:
wiremock:
image: wiremock-extension-oidc
container_name: line-login-wiremock
ports:
- "8081:8080"
command:
- "--global-response-templating"
- "--extensions"
- "org.example.IdTokenTransformer"
- "--verbose"
volumes:
- ./wiremock:/home/wiremock
environment:
OIDC_CLIENT_ID: "1234567890"
OIDC_CLIENT_SECRET: "xxxxxxxxxxyyyyyyyyyyzzzzzzzzzz12"
OIDC_ISSUER: "http://127.0.0.1:8081"
OIDC_JWK_KID: "test-kid-1"
アプリケーション側の設定ファイルは以下のようなイメージです。
application-local.yaml
spring:
application:
name: lineLoginSample
security:
oauth2:
client:
provider:
line:
issuer-uri: http://127.0.0.1:8081
registration:
line:
client-id: 1234567890
client-secret: xxxxxxxxxxyyyyyyyyyyzzzzzzzzzz12
最終的にディレクトリ構成は以下になっている想定です。
line-login-sample
|- src:アプリのコード格納先
|- wiremock
|- mappings
|- authorize.json
|- discovery.json
|- token.json
|- userinfo.json
|- docker-compose.yaml
あとは、コンテナを起動します。
docker compose up -d
SpringBootのアプリも起動し、LINEログインボタンから、ログイン成功の画面に遷移すれば成功です。
OIDCの流れの整理
ざっくりとOIDCの流れとしては以下のようになっています。
プロバイダ(LINE、今回はWiremock) SpringBoot
| <---- request : issuer_url/.well-known/openid-configuration |:アプリ起動時
| response : discovery.json -----------------------> |:endpointを保持
| <---- request : /authorize?... |:LINEログイン押下時
| response : authorize.json -----------------------> |:stateの検証
| <---- request : /token?... |
| response : token.json -----------------------> |:トークンの検証
| <---- request : /useringo |
| response : useringo.json -----------------------> |
SpringSecurityを使って実装しておけば、Webアプリを起動した際に設定ファイルのissuer_url
から、各種エンドポイントを取得します。LINEログインボタン押下時には/authorize
のエンドポイントにリクエストをなげ、その際にstate
とnonce(code)
は自動でランダムな値を生成してくれます。その後の検証周りも、フレームワークが自動で実現してくれます。
LINEの場合、署名のアルゴリズムにHS256方式(共通鍵方式)を使うため、クライアントシークレットの値を使ってトークンの検証ができますが、これがRS256のような、公開鍵を使う方式の場合は、/token
のリクエストの後に/jwks
のリクエストを投げて、公開鍵を取得するフローが追加されます。
感想
今回、ローカルでのLINEログインを実装をするにあたって、実現するまでにAIを使いながらかなり試行錯誤しました。最終的には思ったよりもシンプルに実現することができたつもりですが、この段階に持ってくるまでにかなり苦戦しました。
試行錯誤している途中の段階では、実現したいことと、かかった労力が見合わないなと思う部分もありました。
しかし、実現できるようになって、試行錯誤の過程を振り返る中で様々な知識不足を認識することできて、新しい知識を増やすことができました。具体的には以下のような知識。
- OIDCによる認証の流れ
- IDトークンの署名の仕方
- 署名時の暗号鍵方式の違いによる認証の流れの違い
- 署名のアルゴリズムの違い
- Mavenでのサブモジュールの作り方
- Mavenでのビルドに関する知識
- Javaコンパイラのバージョンの指定や、システムのライブラリも同時にjarにする方法など
- Wiremockによるカスタムレスポンスの作り方や設定方法
結果として得られた知識は多く、特にWiremockのカスタムレスポンスに関しては、APIのテストを自動化する際に役立ちそうな知識だなと思います。
また、OIDCの流れを知ったことで、フレームワークの便利さをより実感することが出来ました。