コンテンツにスキップ

トークンのリフレッシュのしくみ

OAuth の認証情報のアクセストークンは、フローやファンクションのジョブの中で認証情報を使うとき(スクリプトで await credential() を呼んだとき)に、期限が近ければ自動でリフレッシュされます。リフレッシュはランナーの上で行われ、新しいトークンは暗号化したうえで Synqlet に書き戻されます(Synqlet のサーバーは秘密鍵を持たないため、トークンの中身を読めません)。

リフレッシュの様子は、認証情報の詳細画面の「使用履歴」タブで確認できます(有効期限と使用履歴を確認する)。

ジョブの中の認証情報モジュールは、ランナーキー(X-RUNNER-Key ヘッダー)で次の API を呼びます。{id} は認証情報の ID です。

API 役割
POST /api/credentials/{id}/use 暗号化された認証情報を受け取る。あわせて、リフレッシュが必要かどうかを Synqlet が有効期限から判断して返す(下の表)。使用履歴に「使用開始」が残る
POST /api/credentials/{id}/refresh リフレッシュした認証情報と有効期限を書き戻し、ロックを外す。使用履歴に「リフレッシュ」が残る
POST /api/credentials/{id}/refresh-lock/release ロックを受け取ったがリフレッシュしなかったときに、ロックを返す
POST /api/credentials/{id}/refresh-failure リフレッシュに失敗したときに、失敗の理由を伝えてロックを返す。使用履歴に「リフレッシュ失敗」が残る

use の応答の refresh は次のどれかです。

refresh 意味 ジョブがすること
NotNeeded 有効期限に余裕がある 受け取ったトークンをそのまま使う
Acquired 期限が近いので、リフレッシュのロックを渡した 連携先でリフレッシュして refresh で書き戻す
Locked ほかのジョブがリフレッシュ中 1 秒待ってから、もう一度 use を呼ぶ

アクセストークンの有効期限は、暗号化した認証情報とは別に Synqlet が保存しているので、リフレッシュが必要かどうかは Synqlet が判断できます。ジョブもこの有効期限に従います。有効期限が分からない認証情報(この仕組みができる前に連携して、まだリフレッシュしていないものなど)では Acquired を返し、判断はトークンを復号できるジョブに任せます。ジョブは暗号化された認証情報の中にある期限を見て、余裕があればリフレッシュせずに refresh-lock/release でロックを返し、そのとき読み取った有効期限も Synqlet に伝えます。リフレッシュした場合も、ロックを返した場合も有効期限が保存されるので、以後は Synqlet が判断します。

連携先が有効期限を返さない(期限の無いトークンを発行する)場合は、連携したときに「期限なし」として保存され、Synqlet はリフレッシュせずにそのまま使わせます(NotNeeded)。ただし、この仕組みができる前に連携したものは期限が無いことを Synqlet が知らないため、毎回ジョブが判断することになります。気になる場合は再連携してください。

連携先へのリフレッシュは、認証情報タイプに設定されたトークン URL へ grant_type=refresh_token を送って行います。

各 API の詳しい入出力は API リファレンス を参照してください。

複数のジョブが同じ認証情報を使うとき

Section titled “複数のジョブが同じ認証情報を使うとき”

連携先の多くは、リフレッシュのたびにリフレッシュトークンを新しいものに差し替えます。2 つのジョブが同時に古いリフレッシュトークンでリフレッシュすると、遅れた側は使えなくなったトークンを送ることになり、失敗してしまいます。

そのため Synqlet では、リフレッシュできるのは一度に 1 つのジョブだけにしています。リフレッシュが必要なとき、Synqlet は最初に来たジョブにだけ「ロック」を渡し、ほかのジョブには待つように返します。待っているジョブは、書き戻された新しいトークンをそのまま使うので、連携先へのリフレッシュは 1 回で済みます。

sequenceDiagram
    participant A as ジョブA
    participant B as ジョブB
    participant S as Synqlet
    participant P as 連携先

    A->>S: 認証情報を読む<br/>POST /api/credentials/{id}/use
    Note over S: 期限が近い → ジョブAにロックを渡す
    S-->>A: 認証情報 + refresh: Acquired
    B->>S: 認証情報を読む<br/>POST /api/credentials/{id}/use
    S-->>B: refresh: Locked(ジョブAがリフレッシュ中)

    A->>P: トークンをリフレッシュ<br/>POST {トークンURL}(grant_type=refresh_token)
    loop 1秒おきに読み直す
        B->>S: POST /api/credentials/{id}/use
        S-->>B: refresh: Locked
    end
    P-->>A: 新しいトークン
    A->>S: 新しいトークンを書き戻す(ロックを外す)<br/>POST /api/credentials/{id}/refresh

    B->>S: POST /api/credentials/{id}/use
    Note over S: ジョブAが書き戻した期限には余裕がある
    S-->>B: 新しいトークン + refresh: NotNeeded
    Note over B: 連携先へは行かず、そのまま使う

リフレッシュが必要かどうかの判断とロックの受け渡しは、Synqlet の中で一度に行います。そのため「読んだ時点では期限が近かったが、ロックを取るまでの間にほかのジョブがリフレッシュを終えていた」という行き違いは起きません。