SpringSecurityでLINEログインを実現する

SpringBootで構築したWebアプリに対して、SpringSecurityによるLINEログインを実装した際の学び、及び備忘録です。

※SpringBootに関しては簡単に解説している部分もありますが、最低限の触れたことがある人向けの記事になります。

基礎知識の整理

まずは必要になる基礎知識を整理。

認証・認可周り

OAuth2.0

「認可」の仕組みを標準化したもの。
「認証」と「認可」の違いは、「認証がユーザーの本人確認をするもの」、「認可はアクセス制御などの権限管理を行うもの」という違いです。
例えば、とあるアプリがGoogleカレンダーにアクセスする許可を与えたりするのが認可。

OIDC(OpenID Connect)

OAuth2.0に認証の仕組みを追加した拡張仕様。 OIDCを使うことで、IDの連携ができるようになったり、SSOが実現できるようになる。

SSOについては前回の記事でも軽く触れているので、そちらを参照いただければと思います。

JWT(Json Web Token)

OAuth2.0やOIDCでは、トークンを検証することで認証や認可を行う。
OAuth2.0ではアクセストークン、OIDCではIDトークンを検証する。
アクセストークンはJWTが必須ではないが、JWTで表現することも多い。
IDトークンは必ずJWT形式である必要がある。

JWTの検証時に改ざんされていないことを保証するために、署名アルゴリズムを用いる。
この時のアルゴリズムの種類としてHS256(対象鍵方式:HMAC-SHA256)と、RS256(非対称鍵方式:RSA-SHA256)などがある。

Spring周り

SpringBoot

JavaやKotlinでWebアプリケーションを簡単に開発するためのフレームワーク。

SpringSecurity

SpringFrameworkで提供されている認証・認可の基盤。 予め多くの機能が提供されているため、認証認可に関するロジックを自前で実装することなく、ビジネスロジックの開発に注力できる。 提供されているインタフェースやクラスを拡張することで様々なカスタマイズが可能。

LINE周り

LINE Developers

LINEのサービスと自作のアプリケーションを連携させるためのAPIやツール、管理画面などを提供するポータルサイトのこと。連携できるサービスとして「LINE ログイン」「Messaging API」などがある。

LINE ログイン

LINEアカウントを使ってSSOを実現する技術。 LINE公式アカウント、LNE Messaging APIとの連携が簡単に実現できる。
Messaging APIと連携することで、LINEログイン時にLINE公式アカウントの友達追加を促すことができる。

プロバイダー

LINE Developersの中で組織やグループ、個人を表す単位。
LINE Developersのサービスを利用するには必ずプロバイダが必要。
個人のLINEアカウントでログインした場合は、そのアカウントがプロバイダになる。

チャネル

実際にLINEの機能を使うアプリケーションの単位。
「LINE ログイン」「Messaging API」など、連携する際はそれぞれのアプリ単位・サービス単位でチャネルを作成する必要がある。

環境の用意

SpringBootプロジェクトの作成

SpringBootのプロジェクトはSpring Initializrを使って作成します。
今回作成したプロジェクトの詳細は以下。

  • Project:Maven
  • Language:Kotlin
  • SpringBoot 3.5.6

Metadataのnameなどは任意ですが、ここではlineLoginSampleにしました。

依存関係の追加

プロジェクト作成時に以下の依存関係を追加できるので、以下を追加。

  • SoringWeb
  • Thymeleaf
  • Spring Security
  • OAuth2 Client

※後から追加することも可能。

依存関係を追加したら、「GENERATE」ボタンをクリックしてプロジェクトをダウンロードします。

IDEで開く

IDEは任意ですが、私はIntelliJ IDEAを使用しました。

手動で依存関係の追加

プロジェクトを開いたら、pom.xmlに以下の依存関係を追加します。

※こちらの依存関係はSpring Initializrでは追加できなかったので、後から手動追加します。
※この依存関係がないと、LINEログイン時にJWTを処理できずにエラーになります。

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

LINEログインのチャネル作成

続いてはLINE側の準備をします。
LINEログインを使用するには、 LINE Developsのサイトでチャネルを作成します。

LINE developersのページを開いてLINEアカウントでログイン。
コンソール画面のプロバイダーから、「新規チャネル作成」を選択。
「LINEログイン」を選択して、新しいLINEログインのチャネルを作成します。

チャネルの作成が終わったら、「チャネル基本設定」タブから、以下の値を控えておきます。

  • チャネルID
  • チャネルシークレット

また、「LINEログイン設定」タブから、「ウェブアプリでLINEログインを利用する」にチェックを入れる。
そして、コールバックURLにhttp://127.0.0.1:8080/login/oauth2/code/lineをします。

※今回はローカル開発で動作させるためにローカルのIPにしています。
ウェブアプリをサーバーにデプロイして使用する場合はIPの部分をサーバーのドメインに変更します。

実装

設定ファイル

SpringBootプロジェクトのsrc/main/resources/application.yamlを作成し、以下の設定を追加します。

※デフォルトではapplication.propertiesなので、リネームしてyamlに変更します。
application.propertiesのままでも対応可能ですが、重複する項目が多くなるのでyamlにする方が管理しやすくなります。

spring:
  application:  
    name: lineLoginSample  
  security:  
    oauth2:  
      client:  
        provider:  
          line:  
            issuer-uri: https://access.line.me  
        registration:  
          line:  
            provider: line  
            client-id: xxxxxxxxxx  
            client-secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  
            client-authentication-method: client_secret_post  
            authorization-grant-type: authorization_code  
            redirect-uri: http://127.0.0.1:8080/login/oauth2/code/line  
            scope:  
              - profile  
              - openid  
            client-name: LINE

client-idにはLINEログインのチャネルID、client-secretにはLINEログインのチャネルシークレットを設定します。
ただし、ソースコードをバージョン管理する場合、シークレット情報がハードコーディングされた状態はまずいので、環境変数にするか、application-local.yamlなど、環境毎のファイルを用意してバージョン管理対象外にして扱うようにしましょう。

application-local.yaml の例

spring:  
  security:  
    oauth2:  
      client:  
        registration:  
          line:  
            client-id: 1234567890
            client-secret: 12345678901234567890123456789012

この場合、ベースの情報はapplication.yamlが適用され、client-idclient-secretの2項目がこのファイルの値で上書きされます。
このファイルを適用させるには、実行時の引数に--spring.profiles.active=localを指定します。

環境変数にする場合は、以下のように${変数名}とし、環境変数にあらかじめ設定するようにします。

client-id: ${LINE_LOGIN_CLIENT_ID}
client-secret: ${LINE_LOGIN_CLIENT_SECRET}

HTML

ログイン画面を必要最低限の実装で作成します。
LINEログインボタンの画像は以下のサイトよりダウンロードします。

LINEログインボタン デザインガイドライン

LINEログインボタンを追加することによって、ユーザーがLINEログインを利用してアプリにログインできるようになります。img,[object Object]

https://developers.line.biz/ja/docs/line-login/login-button/

line-login.html

<!DOCTYPE html>  
<html lang="ja" xmlns:th="http://www.thymeleaf.org">  
<head>  
    <meta charset="UTF-8">  
    <meta name="viewport" content="width=device-width, initial-scale=1">  
    <title>LINEログイン</title>  
</head>  
<body>  
<div>  
    <h2>LINEログイン</h2>  
    <p th:if="${param.error}">ログイン失敗</p>  
    <div>        
	    <a href="http://127.0.0.1:8080/oauth2/authorization/line">  
	        <img id="btn-line" src="/images/line_login_btn.png">  
        </a>    
    </div>
</div>  
</body>  
</html>

ログインが成功した場合にリダイレクトする画面。
こちらも必要最低限で実装。

login-success.html

<!DOCTYPE html>  
<html lang="ja">  
<head>  
  <meta charset="UTF-8">  
  <title>ログイン成功</title>  
</head>  
<body>  
  <h2>ログイン成功</h2>  
</body>  
</html>

Config

SecurityConfig.kt

package com.example.lineloginsample  
  
import org.springframework.context.annotation.Bean  
import org.springframework.context.annotation.Configuration  
import org.springframework.security.config.annotation.web.builders.HttpSecurity  
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity  
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory  
import org.springframework.security.oauth2.client.registration.ClientRegistration  
import org.springframework.security.oauth2.jose.jws.MacAlgorithm  
import org.springframework.security.oauth2.jwt.JwtDecoderFactory  
import org.springframework.security.web.SecurityFilterChain
  
@Configuration  
@EnableWebSecurity  
class SecurityConfig {  
  
    @Bean  
    fun filterChain(http: HttpSecurity): SecurityFilterChain {  
        http  
            .authorizeHttpRequests { auth ->  
                auth  
                    .requestMatchers("/line-login", "/images/**").permitAll()  
                    .anyRequest().authenticated()  
            }  
            .oauth2Login { oauth2 ->  
                oauth2  
                    .loginPage("/line-login")  
                    .failureUrl("/line-login?error")
                    .defaultSuccessUrl("/login-success", true)
            }  
        return http.build()  
    }
    
    @Bean  
	fun idTokenDecoderFactory(): JwtDecoderFactory<ClientRegistration?> {  
	    val idTokenDecoderFactory = OidcIdTokenDecoderFactory()  
	    idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 }  
	    return idTokenDecoderFactory  
	}
}

コントローラ

LineController.kt

package com.example.lineloginsample  
  
import org.springframework.stereotype.Controller  
import org.springframework.web.bind.annotation.GetMapping  
  
@Controller  
class LineController {  
  
    @GetMapping("/line-login")  
    fun lineLogin(): String {  
        return "line-login"  
    }
    
    @GetMapping("/login-success")  
	fun loginSuccess(): String {  
	    return "login-success"  
	}
}

実装はこれで完了です。
SpringBootのアプリを起動して動かしてみます。

まずはhttp://localhost:8080/line-loginにアクセスします。

以下の画面が表示されました。

LINEログイン前の画面

LINEのログインボタンをクリックすると、以下のようにLINEログイン画面にリダイレクトされます。

ここで、自分のLINEアカウントの情報を入力してログインボタンを押します。

ログイン成功の画面が表示されたので、うまくできていそうです。

やったー!

軽く解説

ログイン画面の解説

簡単に流れを解説。

LINEログインボタンをクリックしたときには以下のようなURLにリダイレクトすることで、LINEログイン画面へリダイレクトすることができます。

https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id=1234567890&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%3Fkey%3Dvalue&state=12345abcde&scope=profile%20openid&nonce=09876xyz

参考:ウェブアプリにLINEログインを組み込む

一方、アプリ側ではLINEログインのボタンは以下のリンクを指定しています。

<a href="http://127.0.0.1:8080/oauth2/authorization/line">

これは、SpringSecurityの機能でうまくリダイレクトされるようになっているみたいです。
application.yamlsecurity.oauth2の設定をしておくことで、上記のURLにリクエストが投げられると、SpringSecurityが自動的にLINEの認可サーバーに対してリダイレクトされる仕組みです。

参考:OAuth2を利用してユーザーをログインする

設定ファイルの解説

設定ファイルに関しても簡単に解説。

改めて、application.yamlは以下。

spring:
  application:  
    name: lineLoginSample  
  security:  
    oauth2:  
      client:  
        provider:  
          line:  
            issuer-uri: https://access.line.me  
        registration:  
          line:  
            provider: line  
            client-id: xxxxxxxxxx  
            client-secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  
            client-authentication-method: client_secret_post  
            authorization-grant-type: authorization_code  
            redirect-uri: http://127.0.0.1:8080/login/oauth2/code/line  
            scope:  
              - profile  
              - openid  
            client-name: LINE

issuer-uriは、OIDCの設定においてよく出てくるキーワードで、OIDCプロバイダ(今回の場合はLINE)が公開している認証サーバーの起点となるURLを指します。
OIDCでは、認証やトークン検証のために、いくつかのエンドポイントが必要になります。
issuer-uriを指定すれば、それらのエンドポイントをまとめて自動解決してくれるが、個別で指定することも可能です。

# issuer-uri: https://access.line.me # 下記の3つをまとめて解決
authorization-uri: https://access.line.me/oauth2/v2.1/authorize # 認証用
token-uri: https://api.line.me/oauth2/v2.1/token # トークン発酵用
jwk-set-uri: https://api.line.me/oauth2/v2.1/certs # トークンが正しいか検証するための公開鍵配布場所

また、scopeでは、profileのみを指定した場合は、OAuth2のプロトコルが使用され、openidを指定することで、OIDCが使用されるようになります。

LINEログインじにそれぞれのスコープで取得可能な情報はこちらを参照。

アルゴリズムの違い

SecurityConfigに以下のBeanがあります。

@Bean  
fun idTokenDecoderFactory(): JwtDecoderFactory<ClientRegistration?> {  
    val idTokenDecoderFactory = OidcIdTokenDecoderFactory()  
    idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 }  
    return idTokenDecoderFactory  
}

これは、署名のアルゴリズムの差異を吸収するための設定。

SpringSecurityは、デフォルトでRS256になっているそう。

参考:Spring Secuity ID トークン検証

一方、LINEプラットフォームはWebアプリに対してはヘッダー情報にアルゴリズムとしてHS256が返されるとのこと。

参考:https://developers.line.biz/ja/docs/line-login/verify-id-token/#header

そのため、アルゴリズムとしてHS256が使用されるように設定することで、JWTの検証が正しく実行されます。
今回の例ではLINEログインのみを実装しようとしているため、この対応で問題ありませんが、複数のOIDCを実装する場合は、それぞれのプロバイダ事のアルゴリズムを確認して適切に対応する必要があります。

デバッグ時の苦労

実はSpringSecurityでLINEログインを実装する際に最も時間がかかったのが、このアルゴリズム差異の調整でした。

試しに、先ほどのBean(SecurityConfigクラスのidTokenDecoderFactoryメソッド)をコメントアウトして、LINEログインを試してみます。

LINEログイン失敗

ログインに失敗してログイン画面に戻ってきます。
しかし、アプリ側のコンソールには何のエラーログも出力されず、エラーの原因が何なのか全く分からず、調査が難航しました。

どうやら、SecurityConfigのfilterChainの方で、認証に失敗した場合にはエラー内容が出力されるように調整する必要があったみたいです。

SecurityConfig.kt

.oauth2Login { oauth2 ->  
    oauth2  
        .loginPage("/line-login")  
        // .failureUrl("/line-login?error") // 元々のコード
        // 以下でエラーが出力されるようにする
        .failureHandler { request, response, exception ->  
            println("OAuth2 Login Failed: ${exception.message}")  
            exception.printStackTrace()  
            response.sendRedirect("/line-login?error")  
        }  
        .defaultSuccessUrl("/login-success", true)  
}

この修正を行った後に再度LINEログインを試してみたところ、以下のようなエラーが出力されました。

OAuth2 Login Failed: [invalid_id_token] An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
org.springframework.security.oauth2.core.OAuth2AuthenticationException: [invalid_id_token] An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
	at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.getJwt(OidcAuthorizationCodeAuthenticationProvider.java:251)
	at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.createOidcToken(OidcAuthorizationCodeAuthenticationProvider.java:238)
	at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.authenticate(OidcAuthorizationCodeAuthenticationProvider.java:156)
	at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
	at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:200)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:239)
	at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:229)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:198)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
	at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
	at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
	at org.springframework.web.filter.ServletRequestPathFilter.doFilter(ServletRequestPathFilter.java:52)
	at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
	at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
	at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebSecurityConfiguration.java:319)
	at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
	at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$4(HandlerMappingIntrospector.java:267)
	at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
	at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
	at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:240)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1776)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:975)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:493)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
	at java.base/java.lang.Thread.run(Thread.java:1570)
Caused by: org.springframework.security.oauth2.jwt.BadJwtException: An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
	at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.createJwt(NimbusJwtDecoder.java:188)
	at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.decode(NimbusJwtDecoder.java:142)
	at org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider.getJwt(OidcAuthorizationCodeAuthenticationProvider.java:247)
	... 73 more
Caused by: com.nimbusds.jose.proc.BadJOSEException: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
	at com.nimbusds.jwt.proc.DefaultJWTProcessor.process(DefaultJWTProcessor.java:357)
	at com.nimbusds.jwt.proc.DefaultJWTProcessor.process(DefaultJWTProcessor.java:303)
	at org.springframework.security.oauth2.jwt.NimbusJwtDecoder.createJwt(NimbusJwtDecoder.java:162)
	... 75 more

このエラーの内容から、SpringBootとLINEでアルゴリズムの違いが出ていることが分かり、それぞれの公式サイトを確認することで対応の方針を見極めることができました。

ログが出力される設定をしていくことは大事。
また、公式ドキュメントをきちんと読むことがやっぱり大事。

ログイン時の処理をカスタマイズ

ここまでの流れでSpringSecurityによるLINEログインが実現できましたが、実際のWebアプリケーションでは、ログイン時にユーザー情報を取得してDBに保存したり、画面上にユーザー情報を表示するなどの要件もあるかと思います。

DBへの保存はここでは扱いませんが、LINEのユーザー名を画面上に表示するまでのカスタマイズを行います。

まずは、ログインユーザーとして保持されるユーザーのクラスを作成します。
OIDCでのログインの場合、OidcUserインターフェースを実装したクラスである必要があります。
今回はとりあえず必要最低限の情報だけ持つように。

LineOidcUser.kt

package com.example.lineloginsample  
  
import org.springframework.security.core.GrantedAuthority  
import org.springframework.security.oauth2.core.oidc.OidcIdToken  
import org.springframework.security.oauth2.core.oidc.OidcUserInfo  
import org.springframework.security.oauth2.core.oidc.user.OidcUser  
  
class LineOidcUser(  
    private val claims: Map<String?, Any?>?,  
    private val userInfo: OidcUserInfo?,  
    private val idToken: OidcIdToken,  
    private val authorities: Collection<GrantedAuthority?>?,  
    private val attributes: Map<String?, Any?>?,  
    private val nameAttributeKey: String,  
): OidcUser {  
    override fun getClaims(): Map<String?, Any?>? {  
        return claims  
    }  
  
    override fun getUserInfo(): OidcUserInfo? {  
        return userInfo  
    }  
  
    override fun getIdToken(): OidcIdToken? {  
        return idToken  
    }  
  
    override fun getAttributes(): Map<String?, Any?>? {  
        return attributes  
    }  
  
    override fun getAuthorities(): Collection<GrantedAuthority?>? {  
        return authorities  
    }  
  
    override fun getName(): String? {  
        return attributes?.get(nameAttributeKey)?.toString()  
    }  
  
    fun getDisplayName(): String? {  
        return attributes?.get("name")?.toString()  
    }  
}

次にログイン時に呼ばれるサービスクラスの実装。
デフォルトではOidcUserServiceloadUserが呼ばれるようになっているので、継承してオーバーライドします。
DBにデータを保存する処理などが必要であれば、この中で実装してあげれば実現可能です。

LineOidcUserService.kt

package com.example.lineloginsample  
  
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest  
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService  
import org.springframework.security.oauth2.core.oidc.user.OidcUser  
import org.springframework.stereotype.Service  
  
@Service  
class LineOidcUserService: OidcUserService() {  
  
    override fun loadUser(userRequest: OidcUserRequest): OidcUser {  
        val oidcUser = super.loadUser(userRequest)  
  
        return LineOidcUser(  
            claims = oidcUser.claims,  
            userInfo = oidcUser.userInfo,  
            idToken = oidcUser.idToken,  
            authorities = oidcUser.authorities,  
            attributes = oidcUser.attributes,  
            nameAttributeKey = "sub"  
        )  
    }  
  
  
}

最後にログイン後の画面でログインしているユーザーの名前が表示されるようにします。

login-successs.html

<!DOCTYPE html>  
<html lang="ja" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">  
<head>  
    <meta charset="UTF-8">  
    <title>ログイン成功</title>  
</head>  
<body>  
    <h2>ログイン成功</h2>  
    <p>ようこそ、<span sec:authentication="principal.displayName"></span> さん</p>  
</body>  
</html>

これで、ログイン後は以下のようにユーザー名が表示されるようになります。

参考

ウェブアプリにLINEログインを組み込む

このページでは、OpenID Connectプロトコルをサポートし、ユーザー情報をIDトークンで取得できるLINEログインLINEログインを組み込めるアプリがない場合は、サンプルアプリを利用できます。「LINEログインを始めよう」を参照してください。

https://developers.line.biz/ja/docs/line-login/integrate-line-login/