LoginLingo

ドキュメント

標準の OpenID Connect なので、既存のOIDCライブラリがそのまま使えるよ

エンドポイント

Issuerhttps://loginlingo.pages.dev
Discovery/.well-known/openid-configuration
認可/authorize
トークン/token
UserInfo/userinfo
JWKS/jwks.json
デバイス/device_authorization → /device
取り消し/revoke(リフレッシュトークン)
ログアウト/logout?client_id=…&post_logout_redirect_uri=…

scope

openidid_token を発行(sub = 百リンゴのユーザー名)
profilepreferred_username, name, profile(プロフィールURL), bio(ひとこと), avatar(アイコン番号), created_at, staff
hyaku.statshyaku.stats:xp, weekly_xp, streak_days, cleared, karuta_wins, gems, pro, deco(かざり)
hyaku.planhyaku.plan:pro_until, lite_until, infinity_until(UNIX秒)
hyaku.friendshyaku.friends:フレンドのユーザー名
hyaku.progressAPI のみ:ステージごとの正解数
hyaku.decksAPI のみ:じぶんの問題(デッキ・表/裏・ダミー選択肢・共有コード)
hyaku.profile.writePATCH /api/v1/me/profile {bio, avatar} でひとこと・アイコンを変更
nekomaru.read / nekomaru.writeねこまる掲示板API(nekomaru.i2p.workers.dev)に access_token をそのまま使える。read=自分向け表示、write=投稿・返信・いいね・自分の投稿の削除
desilingo.read / .write / .admindesilingo API に access_token を使える。read=承認状態・ガイド・Zip、write=利用リクエスト、admin=運営(tka)の承認・見送り
offline_accessrefresh_token を発行(30日・使うたびに入れ替わる)

データは百リンゴから毎回最新を取得(30秒キャッシュ)。

トークンの確認(introspection)

POST /introspect に token=<access_token> → {active, sub, scope, client_id, exp}。取り消し済みのアプリは active:false。自分のAPIサーバーでトークンを確かめるのに使ってね。

リソースAPI

Authorization: Bearer <access_token> で GET。scope が足りないと 403 insufficient_scope。

/api/v1/meuserinfo と同じ
/api/v1/me/statshyaku.stats
/api/v1/me/planhyaku.plan
/api/v1/me/friendshyaku.friends
/api/v1/me/progresshyaku.progress
/api/v1/me/deckshyaku.decks

ルール

・公開クライアント(SPA/スマホ/CLI)は PKCE S256 必須。機密クライアントは client_secret_basic / post。
・戻り先は完全一致。公開クライアントの http://127.0.0.1 だけはポート自由(RFC 8252)。
・認可コードは2分・1回きり。アクセストークンはRS256 JWT(1時間, aud = client_id)。
・ユーザーが連携を解除すると、そのアプリのトークンはすぐ使えなくなる(userinfo / refresh)。

Webアプリ(認可コード + PKCE)

// 1. ログインボタン
const v = base64url(crypto.getRandomValues(new Uint8Array(32)));
const c = base64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(v)));
location = "https://loginlingo.pages.dev/authorize?" + new URLSearchParams({
  response_type: "code", client_id: "ll-xxxxxxxx",
  redirect_uri: "https://example.com/callback",
  scope: "openid profile", state, nonce,
  code_challenge: c, code_challenge_method: "S256" });

// 2. callback で
const t = await fetch("https://loginlingo.pages.dev/token", { method: "POST",
  body: new URLSearchParams({ grant_type: "authorization_code", code,
    redirect_uri: "https://example.com/callback",
    client_id: "ll-xxxxxxxx", code_verifier: v }) }).then(r => r.json());
// t.id_token / t.access_token

CLI(デバイスフロー)

curl -s https://loginlingo.pages.dev/device_authorization \
  -d client_id=ll-xxxxxxxx -d scope="openid profile"
# → user_code を表示して、ユーザーに /device で入れてもらう

curl -s https://loginlingo.pages.dev/token -d client_id=ll-xxxxxxxx \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d device_code=lld_...   # interval 秒ごとに

ボタン

百リンゴでログイン

<a href="…/authorize?…" style="display:inline-flex;align-items:center;gap:10px;
 background:#4b3a70;color:#fff;font-weight:800;border-radius:16px;padding:0 20px;
 height:48px;text-decoration:none;box-shadow:0 4px 0 #2e2347">
 <img src="https://loginlingo.pages.dev/cat.svg" width="28" alt="">百リンゴでログイン</a>
アプリを登録する