業務システム

LINEログイン APIの実装手順:PKCE・IDトークン検証と既存会員IDの連携

LINEログイン APIの実装手順:PKCE・IDトークン検証と既存会員IDの連携

LINEログイン APIは、自社のWebサイトやアプリにLINEアカウントでのログインを組み込むための、OAuth 2.0とOpenID Connectに準拠したAPIです。この記事では、v2.1の認可エンドポイントへstate・nonce・PKCEを付けてリダイレクトするところから、tokenエンドポイントでの認可コードの交換、IDトークンの検証、既存の会員IDとLINEユーザーIDの紐づけまでを、FlaskとPyJWTのコードで順に実装します。ウェブログインとLIFFで署名方式が変わる落とし穴、退会時に呼ぶ権限取消のAPIも扱います。LINEミニアプリとして作るかWebアプリとして作るかの判断は、LINEミニアプリとLIFFの使い分けを整理した記事を先に読むと迷いません。

まとめ:LINEログインAPIは認可コード+PKCEで受け、IDトークンをサーバーで検証する

LINEログインが対応するレスポンスタイプは code だけです。実装は、認可URLへリダイレクトし、コールバックで受けた認可コードを /oauth2/v2.1/token でアクセストークンとIDトークンに換える、という標準の認可コードフローになります。stateとnonceは毎回ランダムに作り、PKCEの code_challenge も付けてください。

ログインした人を識別する値は、IDトークンの sub(LINEユーザーID)です。ウェブログインのIDトークンはHS256でチャネルシークレットを鍵に署名されているため、公開鍵で検証しようとすると失敗します。自前で検証するのが不安なら、verifyエンドポイントにIDトークンとチャネルIDを送るだけで検証できます。

会員テーブルの主キーは自社の会員IDに置き、LINEユーザーIDは別テーブルで紐づけます。退会時には /user/v1/deauthorize で連動アプリの権限を取り消すことが開発ガイドラインで必須とされています。

LINEログインチャネルを作る前に決めるプロバイダーとスコープの設定

プロバイダー単位で変わるユーザーIDとMessaging APIチャネルとの同居

LINE Developersコンソールでは、プロバイダーの下にLINEログインチャネルを作ります。ここで最初に決めるべきは、どのプロバイダーに置くかです。ユーザーIDの説明によると、ユーザーIDは同じ人でもプロバイダーごとに別の値が発行され、形式は U[0-9a-f]{32}(Uと16進数32桁)です。LINEのOpenID設定でも subject_types_supported は pairwise になっています。

逆に、同じプロバイダーであれば、LINEログインチャネルとMessaging APIチャネルで同じユーザーIDが割り当てられます。ログインした会員へ後から公式アカウントで通知を送る計画があるなら、両方のチャネルを同じプロバイダーに置いてください。別プロバイダーに作ってしまうと、ログインで保存したIDがpushの宛先に使えません。送信側の実装はMessaging APIのプッシュメッセージ送信を実装した記事で扱っています。

コールバックURLの登録とprofile・openid・emailスコープの申請手順

コールバックURLは、チャネルの[LINEログイン設定]タブで改行区切りに複数登録できます。認可リクエストの redirect_uri は、登録したURLと完全一致するか、それにクエリパラメータを足した形でなければなりません。ウェブアプリへの組み込み手順にあるscopeの組み合わせは次のとおりです。

scope 取得できる情報 前提
openid ユーザーID なし
profile openid ユーザーID・表示名・プロフィール画像URL なし
openid email ユーザーID・メールアドレス メールアドレス取得権限の申請
profile openid email 上記すべて メールアドレス取得権限の申請

emailを使うには、チャネル基本設定タブの「OpenID Connect」から申請し、メールアドレスの取得と利用目的をユーザーに示す画面のスクリーンショットを提出します。emailだけを指定し openid を外すと INVALID_SCOPE で弾かれます。なお、トークン応答の scope には、emailを許可されていても email の文字は入りません。

認可リクエストからトークン発行までをFlaskとPKCEで実装する手順

state・nonce・code_verifierを作り認可URLへ送る実装

ログインボタンの遷移先で、3つの乱数を作ってセッションに保存し、https://access.line.me/oauth2/v2.1/authorize へリダイレクトします。PKCE対応の説明では、code_verifier は英数字と -._~ の43〜128字、code_challenge_method は S256 のみ(plainは不可)というパラメータの指定です。PyJWT 2.15系(2026年9月公開・Python 3.9以上)とFlaskで書くと次のようになります。

# pip install flask requests "pyjwt[crypto]"
import base64
import hashlib
import secrets
import urllib.parse

import jwt
import requests
from flask import Flask, abort, redirect, request, session

app = Flask(__name__)
app.secret_key = "..."          # 環境変数やシークレット管理から読み込む
CHANNEL_ID = "1234567890"       # LINEログインチャネルのチャネルID
CHANNEL_SECRET = "..."          # チャネルシークレット(クライアントに出さない)
REDIRECT_URI = "https://example.com/callback"
AUTHORIZE_URL = "https://access.line.me/oauth2/v2.1/authorize"
TOKEN_URL = "https://api.line.me/oauth2/v2.1/token"


@app.get("/login")
def login():
    state = secrets.token_urlsafe(32)
    nonce = secrets.token_urlsafe(32)
    verifier = secrets.token_urlsafe(64)  # 86字。使える文字だけで構成される
    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
    session.update(line_state=state, line_nonce=nonce, line_verifier=verifier)
    params = {
        "response_type": "code",
        "client_id": CHANNEL_ID,
        "redirect_uri": REDIRECT_URI,
        "state": state,
        "scope": "profile openid",
        "nonce": nonce,
        "code_challenge": challenge,
        "code_challenge_method": "S256",
    }
    query = urllib.parse.urlencode(params, quote_via=urllib.parse.quote)
    return redirect(f"{AUTHORIZE_URL}?{query}")

quote_via=urllib.parse.quote を指定しているのは、scopeの区切りを + ではなく %20 にするためです。stateはログインのたびに作り直し、Cookie署名つきのセッションなど第三者が読めない場所に置きます。この2点はLINEログインのセキュリティチェックリストの項目そのものです。PKCEが防ぐ攻撃の中身はPKCEのcode_verifierと検証設計を解説した記事に詳しくまとめています。

コールバックでstateを照合しtokenエンドポイントで認可コードを交換する

コールバックでは、まずstateを照合し、一致しなければ何もせずに止めます。そのうえで認可コードを application/x-www-form-urlencoded で送ります。LINEログイン v2.1 APIリファレンスでは、PKCEを使う場合も client_secret は省けません。

@app.get("/callback")
def callback():
    expected = session.pop("line_state", None)
    if not expected or not secrets.compare_digest(request.args.get("state", ""), expected):
        abort(400)  # stateが無い・一致しない場合は処理しない
    if "error" in request.args:
        # ACCESS_DENIED などはログイン画面へ戻す
        return redirect("/login-failed")

    res = requests.post(TOKEN_URL, data={
        "grant_type": "authorization_code",
        "code": request.args["code"],
        "redirect_uri": REDIRECT_URI,
        "client_id": CHANNEL_ID,
        "client_secret": CHANNEL_SECRET,
        "code_verifier": session.pop("line_verifier"),
    }, timeout=10)
    res.raise_for_status()
    tokens = res.json()  # access_token・expires_in・id_token・refresh_token・scope・token_type

    claims = verify_web_id_token(tokens["id_token"], session.pop("line_nonce"))
    member_id = find_or_link_member(claims["sub"])  # 次章の紐づけ処理
    session["member_id"] = member_id
    return redirect("/mypage")

応答の access_token は30日、refresh_token は発行から最長90日有効です。ログイン判定だけが目的ならアクセストークンを長く持つ必要はありませんが、後述の退会処理で使うため、会員と紐づけて暗号化して保存しておく設計にしています。リファレンスは応答のプロパティが将来増える可能性に触れているので、JSONを厳密なスキーマで受けないでください。

ACCESS_DENIEDなどのエラー応答と10分で失効する認可コードの扱い

同意画面でキャンセルされると、コールバックに error=ACCESS_DENIED と送ったstateが返ります。組み込み手順に載っているエラーコードは7種類で、実装上の扱いは2つに分かれます。

  • 利用者の操作によるもの:ACCESS_DENIED(同意の拒否)、LOGIN_REQUIRED と INTERACTION_REQUIRED(prompt=none で自動ログインできなかった)
  • 実装やサーバーの問題:INVALID_REQUEST、UNSUPPORTED_RESPONSE_TYPE、INVALID_SCOPE、SERVER_ERROR

前者はログイン画面へ戻して別の手段を示し、後者はアラートの対象にします。client_id や redirect_uri が誤っているときはコールバックへ戻らずLINE側の画面で止まるため、ログに何も残りません。認可コードの有効期間は10分で、使えるのは1回だけです。ブラウザの戻る操作でコールバックが再実行されても、前述のコードではstateをセッションから取り出し済みのため400で止まります。この400はログイン画面へ戻す扱いで足ります。

IDトークンをverifyエンドポイントとPyJWTで検証するコードと署名方式

verifyエンドポイントにid_tokenとnonceを送るcurlと6種類のエラー

いちばん手堅いのは、LINEのverifyエンドポイントに検証を任せる方法です。IDトークンの検証の説明では、LINEプラットフォームから直接受け取った場合を除き、IDトークンはサーバーで検証するよう求めています。

curl -s -X POST https://api.line.me/oauth2/v2.1/verify \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode "id_token=${ID_TOKEN}" \
  --data-urlencode "client_id=${LINE_CHANNEL_ID}" \
  --data-urlencode "nonce=${NONCE}"

成功すると iss・sub・aud・exp・iat・nonce・amr・name・picture などを含むJSONが返ります。失敗時の error_description は、署名不正の Invalid IdToken.、発行者違いの Invalid IdToken Issuer.、期限切れの IdToken expired.、チャネルID違いの Invalid IdToken Audience.、nonce違いの Invalid IdToken Nonce.、user_id 違いの Invalid IdToken Subject Identifier. の6種類です。HTTPを1往復増やす代わりに、鍵の扱いを持たずに済みます。

ウェブログインはHS256、LIFFとSDKはES256になる署名方式の分岐

自前で検証するなら、署名方式の違いに注意してください。ウェブログインで得たIDトークンはHS256でチャネルシークレットを鍵に署名され、ヘッダに kid がありません。LIFFアプリやネイティブアプリのLINE SDKで得たものはES256で、kid に一致する公開鍵を JWKS から引きます。紛らわしいのは、OpenID設定の id_token_signing_alg_values_supported にES256しか書かれていない点です(2026年11月5日取得)。設定だけを見てES256で検証すると、ウェブログインのトークンは必ず失敗します。

ISSUER = "https://access.line.me"
jwks_client = jwt.PyJWKClient("https://api.line.me/oauth2/v2.1/certs")


def verify_web_id_token(id_token, expected_nonce):
    """ウェブログインのIDトークン(HS256・鍵はチャネルシークレット)"""
    claims = jwt.decode(id_token, CHANNEL_SECRET, algorithms=["HS256"],
                        audience=CHANNEL_ID, issuer=ISSUER)
    if not secrets.compare_digest(claims.get("nonce", ""), expected_nonce):
        raise jwt.InvalidTokenError("nonce mismatch")
    return claims


def verify_liff_id_token(id_token):
    """LIFF・LINE SDKのIDトークン(ES256・鍵はJWKSのkid一致分)"""
    signing_key = jwks_client.get_signing_key_from_jwt(id_token)
    return jwt.decode(id_token, signing_key.key, algorithms=["ES256"],
                      audience=CHANNEL_ID, issuer=ISSUER)

algorithms は経路ごとに1つだけ指定し、両方を許す書き方はしないでください。HS256とES256を同じ関数で受け付けると、鍵の取り違えを狙う攻撃の余地が生まれます。exp の確認は jwt.decode が既定で行います。JWTの署名検証そのものの考え方はJWTの構造と署名検証の仕組みを解説した記事、検証手順の一般形はOpenID Connect Core 1.0の「ID Token Validation」が原典です。

profile・userinfo・IDトークンで取得できるユーザー情報の違い

ログイン後にユーザー情報を取る経路は4つあり、必要なスコープと返る項目が少しずつ違います。リファレンスの記載を並べると次のとおりです。

  • IDトークン(verifyエンドポイント含む):scopeは openid。sub・name・picture・email・amr が入る
  • GET /oauth2/v2.1/userinfo:scopeは openid。sub・name・picture を返す
  • GET /v2/profile:scopeは profile。userId・displayName・pictureUrl・statusMessage を返す
  • GET /friendship/v1/status:scopeは profile。公式アカウントと友だちかを friendFlag で返す

メールアドレスが返るのはIDトークンだけです。ログイン直後の会員登録にはIDトークンのクレームで足り、APIを追加で呼ぶ必要はありません。/v2/profile を使うのは、ステータスメッセージが要る場合か、保存したアクセストークンで後から最新の表示名を取り直す場合に限られます。amr には pwd・lineautologin・lineqr・linesso・mfa が入りえますが、条件によって含まれないことがあるため、必須項目として扱わないでください。

既存会員IDとLINEユーザーIDを紐づけるテーブル設計と退会時の権限取消

会員テーブルの主キーは自社IDに置きLINEのsubを別テーブルで持つ設計

LINEユーザーIDを会員テーブルの主キーにすると、プロバイダーを移したときや、LINE以外のログイン手段を足したときに全テーブルの外部キーを書き換えることになります。主キーは自社で採番し、LINEとの対応は別テーブルに切り出します。

CREATE TABLE members (
  id          BIGINT PRIMARY KEY,
  email       VARCHAR(255),
  created_at  TIMESTAMP NOT NULL
);

CREATE TABLE member_line_links (
  member_id          BIGINT NOT NULL REFERENCES members(id),
  line_user_id       CHAR(33) NOT NULL UNIQUE,  -- IDトークンのsub(U+16進数32桁)
  login_channel_id   VARCHAR(20) NOT NULL,      -- プロバイダーの取り違え調査用
  access_token_enc   TEXT,                      -- 退会時のdeauthorizeに使う(暗号化して保存)
  linked_at          TIMESTAMP NOT NULL,
  UNIQUE (member_id)
);

line_user_id に一意制約を付け、1つのLINEアカウントが2人の会員に紐づく状態をDBで防ぎます。member_id にも一意制約を付けているのは、1人の会員に1つのLINEアカウントという運用を前提にしているためで、複数のLINEアカウントを許すならこの制約を外します。

メールアドレス一致で自動統合しない理由とログイン中に連携させる流れ

既存会員がLINEログインを初めて使ったとき、IDトークンのemailと会員テーブルのemailが一致したら同一人物とみなして紐づけたくなります。この自動統合は採用しません。LINEアカウントに登録されたメールアドレスを、その人が今も持っているとは限らないからです。既存アカウントの乗っ取りにつながる経路を自分で作ることになります。

既存会員の連携は、自社のIDとパスワードでログインした状態のマイページに「LINEと連携する」ボタンを置き、そこからLINEログインを通す流れにします。コールバックでは、セッションの member_id と sub を member_line_links に書き込むだけです。未ログインでLINEログインを通った人のsubが未登録なら、新規会員として登録するか、既存会員なら先にログインしてから連携するよう案内します。クライアントからユーザーIDだけを送らせて紐づける実装は、チェックリストが禁じている形です。必ず生のIDトークンかアクセストークンをサーバーへ送り、サーバー側で検証します。

退会・連携解除でdeauthorizeを呼ぶ必須事項とログに残す4項目

LINEログイン開発ガイドラインは、ユーザーが退会したときや連携を解除したときに、連動アプリへの権限を取り消すことを必須事項にしています。使うのは /user/v1/deauthorize で、ヘッダにチャネルアクセストークン(任意の有効期間を指定できるv2.1かステートレス)、ボディにユーザーのアクセストークンを入れます。

curl -s -o /dev/null -w "%{http_code}\n" -X POST https://api.line.me/user/v1/deauthorize \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${CHANNEL_ACCESS_TOKEN}" \
  -d "{\"userAccessToken\": \"${USER_ACCESS_TOKEN}\"}"
# 204 なら取消完了。400 invalid token は既に取り消し済みのことが多い

ユーザーのアクセストークンが要るため、保存していたトークンが期限切れなら退会の操作時にもう一度LINEログインを通してもらうと確実です。ガイドラインはあわせて、問い合わせ調査のために次のログを一定期間残すよう勧めています。LINE側はログを提供しないので、残さなければ調べようがありません。

  1. 認可リクエストの時刻とパラメータ
  2. 認可コードまたはエラー応答を受け取った時刻・メソッド・内容
  3. LINEログインAPIの x-line-request-id・時刻・メソッド・エンドポイント・ステータスコード
  4. 可能ならAPIのリクエストボディとレスポンスボディ

4つ目にはトークンが含まれるため、保存するならトークン部分を伏せてから書き出してください。

LINEログインを自前で実装する条件と認証基盤サービスに任せる場面

LINEだけを入口にするWeb会員サイトで自前実装を選んでよい条件

LINEログインの概要にあるとおり、LINEログイン自体は無料で使えます。ログイン手段がLINEだけで、会員DBもアプリも自社で持っているなら、この記事のコードの範囲(認可・交換・検証・紐づけ・取消)で完結し、外部の認証サービスを挟む理由はありません。店舗の会員証、予約サイト、LINE公式アカウントと組み合わせた通知つきのサービスがこの形です。

ただし、LINEアプリの中で開く画面を作るならWebのLINEログインではなくLIFFを選びます。LIFFブラウザ内での認可リクエストは動作保証の対象外とされ、外部ブラウザで開くLIFFアプリでも liff.login() を使うよう案内されています。LIFF側の作り方はLIFFの仕組みと開発手順を実装目線で解説した記事を参照してください。LINEログインと会員基盤、Messaging APIの通知までをまとめて設計したい場合は、一創のLINEミニアプリ開発で要件整理から相談できます。

Googleアカウントやメール認証と併用するなら認証基盤を先に置く判断

LINEに加えてGoogleやApple、メールアドレスとパスワードのログインも並べるなら、LINEログインをアプリに直接書くのは見送ります。プロバイダーごとにstate・nonce・PKCE・IDトークン検証・連携テーブル・退会時の取消を作ることになり、署名方式の分岐のような個別の癖もプロバイダーの数だけ抱えるからです。

この場合は、OpenID Connectのカスタムプロバイダーを登録できる認証基盤(Keycloak、Auth0、Amazon Cognitoなど)を先に置き、アプリはその認証基盤とだけ話す構成にします。LINE側の設定は、認証基盤のコールバックURLを登録し、チャネルIDとチャネルシークレットを渡すことが中心です。ウェブログインのIDトークンがHS256である点は認証基盤側でも問題になりうるので、導入前に署名アルゴリズムを指定できるかを確かめてください。OpenID Connectの役割分担はOIDCの仕組みとIDトークンの検証手順を解説した記事が前提知識になります。

よくある質問

LINEログイン APIの実装で、着手前によく出る質問をまとめました。

LINEログインの利用に料金はかかりますか?

かかりません。LINE Developersの概要ページに、LINEログインは無料で利用できると書かれています。費用が発生するのは、ログインした会員にLINE公式アカウントからメッセージを送る場合で、こちらはMessaging APIの通数として料金プランの計算対象です。ログインのたびに友だち追加を促したいときは、認可URLに bot_prompt=normal か aggressive を付けると、リンクした公式アカウントの友だち追加の選択肢をログイン画面に表示できます。

LINEログインで電話番号や住所を取得できますか?

できません。LINEログインで要求できるscopeは profile・openid・email の3つで、取得できるのはユーザーID・表示名・プロフィール画像URL・ステータスメッセージと、申請が通った場合のメールアドレスまでです。電話番号や住所が必要なら、ログイン後の会員登録フォームで本人に入力してもらいます。氏名もLINEの表示名は本名とは限らないため、請求書や配送に使う値は別に取得してください。

LIFFアプリの中でLINEログインの認可URLを開いてもよいですか?

使わないでください。組み込み手順に、LIFFブラウザ内での認可リクエストは動作を保証しないと明記されています。LIFFアプリでは liff.getIDToken() で取ったIDトークンをサーバーへ送り、サーバー側でverifyエンドポイントかES256の公開鍵で検証します。外部ブラウザで開くLIFFアプリの場合も、認可URLを自前で組み立てず liff.login() を呼ぶのが公式の案内です。

バックエンドにはユーザーIDとIDトークンのどちらを送るべきですか?

IDトークンかアクセストークンを、加工せずにそのまま送ります。フロントエンドで取り出したユーザーIDだけを送ると、改ざんされたIDをサーバーが信じてしまうためです。セキュリティチェックリストでも、この点はサーバー側での確認項目です。アクセストークンを送る場合は、サーバーで GET /oauth2/v2.1/verify を呼び、client_id が自社チャネルのIDと一致し、expires_in が正の値であることを確かめてから使います。OAuth 2.0の認可と認証の違いはOAuth 2.0の仕組みを解説した記事で整理しています。

LINEログイン v2.0で作った実装はそのまま使い続けられますか?

新しく手を入れるならv2.1へ移してください。APIリファレンスではv2.0は非推奨とされ、v2.1の利用が推奨されています。IDトークン・emailスコープの申請・PKCEは、いずれもv2.1のドキュメントで説明されている機能です。移行の中心は、認可URLとtokenエンドポイントのパスを v2.1 に変え、IDトークンの検証を足すことです。プロバイダーとチャネルを変えなければユーザーIDは変わらないため、会員との紐づけは引き継げます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  2. 2026.11.04 テックブログ CVE-2026-119977:WordPress更新中のRCEと安全な更新手順
  3. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装
  4. 2026.11.02 テックブログ アララの不正アクセス:メール約7,700万件の持ち出しと契約企業が行うAPIキー再設定・代替送信・連名報告
  5. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号

RELATED POSTS 関連記事

目次