標準の OpenID Connect なので、既存のOIDCライブラリがそのまま使えるよ
| Issuer | https://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=… |
| openid | id_token を発行(sub = 百リンゴのユーザー名) |
| profile | preferred_username, name, profile(プロフィールURL), bio(ひとこと), avatar(アイコン番号), created_at, staff |
| hyaku.stats | hyaku.stats:xp, weekly_xp, streak_days, cleared, karuta_wins, gems, pro, deco(かざり) |
| hyaku.plan | hyaku.plan:pro_until, lite_until, infinity_until(UNIX秒) |
| hyaku.friends | hyaku.friends:フレンドのユーザー名 |
| hyaku.progress | API のみ:ステージごとの正解数 |
| hyaku.decks | API のみ:じぶんの問題(デッキ・表/裏・ダミー選択肢・共有コード) |
| hyaku.profile.write | PATCH /api/v1/me/profile {bio, avatar} でひとこと・アイコンを変更 |
| nekomaru.read / nekomaru.write | ねこまる掲示板API(nekomaru.i2p.workers.dev)に access_token をそのまま使える。read=自分向け表示、write=投稿・返信・いいね・自分の投稿の削除 |
| desilingo.read / .write / .admin | desilingo API に access_token を使える。read=承認状態・ガイド・Zip、write=利用リクエスト、admin=運営(tka)の承認・見送り |
| offline_access | refresh_token を発行(30日・使うたびに入れ替わる) |
データは百リンゴから毎回最新を取得(30秒キャッシュ)。
POST /introspect に token=<access_token> → {active, sub, scope, client_id, exp}。取り消し済みのアプリは active:false。自分のAPIサーバーでトークンを確かめるのに使ってね。
Authorization: Bearer <access_token> で GET。scope が足りないと 403 insufficient_scope。
| /api/v1/me | userinfo と同じ |
| /api/v1/me/stats | hyaku.stats |
| /api/v1/me/plan | hyaku.plan |
| /api/v1/me/friends | hyaku.friends |
| /api/v1/me/progress | hyaku.progress |
| /api/v1/me/decks | hyaku.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)。
// 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
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>アプリを登録する