WebログインとLIFF/SDKにおけるLINEログインのIDトークンの署名方式の違い

WebログインとLIFF/SDKにおけるLINEログインのIDトークンの署名方式の違い

LINEログインで受け取ったIDトークンの署名検証時に、OpenID Configurationには`ES256`と書かれているのに、実物のWebログイントークンは`HS256`で署名されていた—その混乱から、実装方法による署名方式の違いについて調べた過程をまとめました。
2026.06.28

リテールアプリ共創部のるおんです。

LINEログインで受け取った IDトークン の署名を検証しようとしたとき、気になった点がいくつかありました。この記事は、そのとき疑問に思って混乱したところと、調べて理解した流れをまとめたものです。

最初に LINE の OpenID Configuration を見たら、署名アルゴリズムは ES256(公開鍵方式)と書いてありました。なので「JWKS から公開鍵を取ってきて ES256 で検証すればいい」と思って実装したのですが、実際に Webのログインで受け取った IDトークンは HS256(共通鍵方式)で署名されていて、検証が一向に通りませんでした

実際のOpenID Configurationは以下のようになっています。

{
    "issuer": "https://access.line.me",
    "authorization_endpoint": "https://access.line.me/oauth2/v2.1/authorize",
    "token_endpoint": "https://api.line.me/oauth2/v2.1/token",
    "revocation_endpoint": "https://api.line.me/oauth2/v2.1/revoke",
    "userinfo_endpoint": "https://api.line.me/oauth2/v2.1/userinfo",
    "scopes_supported": ["openid", "profile", "email"],
    "jwks_uri": "https://api.line.me/oauth2/v2.1/certs",
    "response_types_supported": ["code"],
    "subject_types_supported": ["pairwise"],
+   "id_token_signing_alg_values_supported": ["ES256"], // 公開鍵方式
    "code_challenge_methods_supported": ["S256"]
}

「configには ES256 って書いてあるのに、なんで実物は HS256 なんだ」としばらく混乱しました。結論から言うと、これは LINEログインの実装方法(Webログイン か LIFF/SDK か)によって署名方式が変わる という仕様が理由でした。

先に結論

  • LINEログインの IDトークンの署名方式は、実装方法によって2種類 ある
    • Webのログイン(ブラウザでの認可コードフロー)→ HS256共通鍵 / チャネルシークレットで署名・検証)
    • LIFF / ネイティブSDK 経由のログイン → ES256公開鍵 / JWKS の公開鍵で検証)
  • ところが OpenID Configuration には ES256 しか書かれていない 。なので「Config を信じて ES256 で検証する」実装にすると、Webログインのトークン(HS256)が検証できずにハマる
  • 見分け方はシンプルで、IDトークンのヘッダーを見ればいいHS256 なら kid が無く、ES256 なら kid があって JWKS の鍵と対応する
Webログイン LIFF / SDK
alg HS256 ES256
鍵の種類 共通鍵(チャネルシークレット) 公開鍵 / 秘密鍵
検証に使うもの チャネルシークレット JWKS(jwks_uri の公開鍵)
ヘッダーの kid 無し 有り(JWKS の鍵IDと一致)

背景:IDトークンの署名を検証したかった

そもそも、なぜ署名を検証するのか。IDトークンは「誰がログインしたか」をアプリに伝えるものなので、「それが確かに LINE が発行した、改ざんされていないトークンか」を検証してから信用する 必要があります。検証せずに中身(sub など)を信じてしまうと、偽造したトークンを掴まされるリスクがあります。

JWT の署名検証では、まず どのアルゴリズム(alg)と、どの鍵で署名されているか を知る必要があります。それは推測するものではなく、IdP(ここでは LINE)が公開している OpenID Configuration を見れば分かる と思っていました。

curl https://access.line.me/.well-known/openid-configuration

返ってきた JSON のうち、署名検証に関係するのはこのあたりです。

OpenID Configuration(抜粋)
{
  "issuer": "https://access.line.me",
  "jwks_uri": "https://api.line.me/oauth2/v2.1/certs",
  "id_token_signing_alg_values_supported": ["ES256"],
  "subject_types_supported": ["pairwise"]
}

id_token_signing_alg_values_supported["ES256"] 。つまり「IDトークンは ES256で署名するよ」と宣言されています。そして jwks_uri検証用の公開鍵 が公開されています。

なので方針は、jwks_uri から公開鍵を取得し、ES256 でIDトークンの署名を検証する ということだと考えました。

ところが、jose ライブラリで ES256 検証を実装して、Webのログインフローで受け取った実物のIDトークンに対して走らせると、検証が通りません

JWSSignatureVerificationFailed: signature verification failed
(または ERR_JOSE_ALG_NOT_ALLOWED)

「鍵の取り違えかな?」と思って、そもそも実物のトークンが何で署名されているのか を確かめることにしました。JWT のヘッダー(1つ目のパート)をデコードします。

echo '<id_token>' | cut -d '.' -f1 | python3 -c "import sys,base64,json;p=sys.stdin.read().strip();print(json.loads(base64.urlsafe_b64decode(p+'='*(-len(p)%4))))"

出てきたのはこれでした。

Webログインで受け取ったIDトークンのヘッダー
{
  "alg": "HS256",
  "typ": "JWT"
}

ES256 ではなく HS256 。しかも、ES256 なら署名検証に使う鍵を指す kid(Key ID)が入っているはずなのに、kid がありません

HS256HMAC-SHA256 、つまり 共通鍵(対称鍵)方式 です。LINEログインで両者が共有している秘密といえば、チャネルシークレット です。試しに、検証する鍵を チャネルシークレット に変えて HS256 で検証し直すと、今度はあっさり通りました。

つまり、OpenID Configuration は ES256 と言っているのに、Webログインの実物は HS256(チャネルシークレットによる共通鍵署名)だった 。ここで完全に「どっちが本当なんだ」と混乱しました。

一次情報を確認する:公式ドキュメント

自分の勘違いやコードのミスを疑い切ったところで、一次情報 を当たることにしました。

https://developers.line.biz/ja/docs/line-login/verify-id-token/#get-an-id-token

LINE の公式ドキュメント「IDトークンを検証する」に、まさにこのことがはっきり書かれていました。

スクリーンショット 2026-06-28 14.06.49

ネイティブアプリやLINE SDK、LIFFアプリに対してはES256(ECDSA using P-256 and SHA-256)が、ウェブログインに対してはHS256(HMAC using SHA-256)が返されます。

検証に使う鍵も、署名方式ごとに分けて書かれています。

  • ES256 の場合 … JWKドキュメント(jwks_uri)の中の、ヘッダーの kid に対応する 公開鍵
  • HS256 の場合 … チャネルシークレット

つまり、同じLINEログインでも、Webでログインしたか、SDK/LIFF でログインしたかで署名方式が変わるということです 。そして OpenID Configuration が公開しているのは ES256 のほうだけ、というのも合点がいきました。

裏取り:LIFF版のトークンも確かめる

ドキュメント上は「LIFF なら ES256」とのことなので、本当にそうなるのか を実物で確かめます。LIFF SDK 経由で取得したIDトークンのヘッダーをデコードしてみます。
普段開発しているLINEミニアプリのネットワークタブからid_tokenを拾ってきました。

LIFF経由で受け取ったIDトークンのヘッダー
{
  "alg": "ES256",
  "typ": "JWT",
  "kid": "95e9119f..."
}

今度は ES256 、そして kid 付き です。この kid が、jwks_urihttps://api.line.me/oauth2/v2.1/certs)が返す公開鍵セットの中の、どの鍵で検証すべきかを指しています。実際に JWKS を取得して kid を突き合わせると、ちゃんと 同じ kid の公開鍵が存在 しました。

ちなみに余談ですが、Web版とLIFF版で 同じ自分のアカウントなのに sub(ユーザー識別子)の値が違う のも確認できました。これは OpenID Configuration の subject_types_supportedpairwise だったこととも整合します。pairwise は「宛先ごとに別々の sub を発行する」方式で、LINE の場合その単位は プロバイダー です。同じプロバイダー配下のチャネルなら sub は同じで、プロバイダーが違えば別の sub になります。こうして、サービスをまたいだ名寄せを防いでいます。
普段LINEミニアプリ開発をする中で、プロバイダーが違えばUIDが異なるということをよく知っていたので、これは合点がいきました。

{
    "issuer": "https://access.line.me",
    "authorization_endpoint": "https://access.line.me/oauth2/v2.1/authorize",
    "token_endpoint": "https://api.line.me/oauth2/v2.1/token",
    "revocation_endpoint": "https://api.line.me/oauth2/v2.1/revoke",
    "userinfo_endpoint": "https://api.line.me/oauth2/v2.1/userinfo",
    "scopes_supported": ["openid", "profile", "email"],
    "jwks_uri": "https://api.line.me/oauth2/v2.1/certs",
    "response_types_supported": ["code"],
+   "subject_types_supported": ["pairwise"], // プロバイダーごとに別々の sub を発行する
    "id_token_signing_alg_values_supported": ["ES256"], // 公開鍵方式
    "code_challenge_methods_supported": ["S256"]
}

なぜ署名方式が分かれているのか

ここまで来ると「なぜそんなややこしい仕様に?」が気になります。共通鍵(HS256)と公開鍵(ES256)の性質を踏まえると、理由が見えてきます。

  • HS256(共通鍵) は、署名にも検証にも 同じ秘密の鍵 を使います。なので 検証する側が秘密鍵(チャネルシークレット)を安全に持てる ことが前提です。Webアプリは サーバーサイドでチャネルシークレットを秘密に保持できる ので、共通鍵方式が成立します。鍵を共有している分、JWKSを取りに行く必要もなく、速くてシンプルです。
  • ES256(公開鍵) は、秘密鍵で署名し、誰でも入手できる公開鍵で検証 します。検証側は秘密を持たなくていいのが利点です。LIFF やネイティブSDK は クライアント(端末)側で動く=チャネルシークレットを安全に隠せない ため、共通鍵を配るわけにはいきません。だから「秘密鍵は LINE 側だけが持ち、検証は公開鍵(JWKS)で行う」公開鍵方式が選ばれている、と理解できます。

つまり、「検証する側がチャネルシークレットを安全に保持できるか」 という実装環境の違いが、署名方式の違いに表れているということで自分は理解しました。

そしてこの「秘密を安全に持てるか」という違いは、OAuth/OIDC でいう コンフィデンシャルクライアント(サーバーを持つWeb)と パブリッククライアント(端末で動くLIFF/SDK) の違いそのものです。秘密を持てる側はバックチャネルでトークンを受け取れるので共通鍵で済み、持てない側はトークンが手元に来る分、公開鍵で検証するしかないです、認可コードフローとインプリシットフローの使い分けと、根っこは同じ問いから来ています。

おわりに

「Config に ES256 と書いてあるのに、Webログインの実物は HS256 だった」という混乱から始まって、検証 → 公式ドキュメント と裏を取っていった結果、「Webログインは共通鍵(HS256/チャネルシークレット)、LIFF/SDK は公開鍵(ES256/JWKS)」 という結論にたどり着きました。

認証まわりはライブラリ任せにしがちですが、「実物をデコードして確かめる」 癖をつけておくと、今回のような Config と実装の食い違いに出くわしても落ち着いて切り分けられます。

以上、どなたかの参考になれば幸いです。

参考

https://developers.line.biz/ja/docs/line-login/verify-id-token/

https://access.line.me/.well-known/openid-configuration

この記事をシェアする

関連記事