STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

OpenID Connect フレームワーク fosite にコントリビュートしました

こんにちは。プロダクト基盤本部 基盤グループの inari111 です。
私の部署は STORES 各プロダクトへ導入する ID 基盤の開発をしています。
今回は ID 基盤内で使われている OpenID Connect フレームワーク ory/fosite にコントリビュートしたことについて書きます。

ory/fosite とは

ory/fosite は OAuth2 & OpenID Connect の Go フレームワークです。
fosite が提供している interface を実装することで OpenID Connect の仕様を満たすことができます。

Pull request を出すきっかけ

リフレッシュトークンの有効期限が切れたときに意図していないエラーメッセージが返っていることがコードレビューで判明しました。
リポジトリを見に行くと Issue は立てられていたのですが Pull request は出されていない状態だったため、修正してみることにしました。

どんな不具合か

RFC6749 を確認したところ、リフレッシュトークンの有効期限切れのときは invalid_grant を返すのが正しそうですが、 invalid_request が返っている状態でした。

invalid_grant
The provided authorization grant (e.g., authorization
code, resource owner credentials) or refresh token is
invalid, expired, revoked, does not match the redirection
URI used in the authorization request, or was issued to
another client.


invalid_request
The request is missing a required parameter, includes an
unsupported parameter value (other than grant type),
repeats a parameter, includes multiple credentials,
utilizes more than one mechanism for authenticating the
client, or is otherwise malformed.

修正内容

出したPull requestはこちらです。
fix: handle invalid_token error for refresh_token is expired by inari111 · Pull Request #664 · ory/fosite

今回の修正で ValidateRefreshTokenfosite.ErrTokenExpired を返したら fosite.ErrInvalidGrant (invalid_grant) を返すようになっています。

// HandleTokenEndpointRequest implements https://tools.ietf.org/html/rfc6749#section-6
func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Context, request fosite.AccessRequester) error {
    if !c.CanHandleTokenEndpointRequest(request) {
        return errorsx.WithStack(fosite.ErrUnknownRequest)
    }

    if !request.GetClient().GetGrantTypes().Has("refresh_token") {
        return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHint("The OAuth 2.0 Client is not allowed to use authorization grant 'refresh_token'."))
    }

    refresh := request.GetRequestForm().Get("refresh_token")
    signature := c.RefreshTokenStrategy.RefreshTokenSignature(refresh)
    originalRequest, err := c.TokenRevocationStorage.GetRefreshTokenSession(ctx, signature, request.GetSession())
    if errors.Is(err, fosite.ErrInactiveToken) {
        // Detected refresh token reuse
        if rErr := c.handleRefreshTokenReuse(ctx, signature, originalRequest); rErr != nil {
            return errorsx.WithStack(fosite.ErrServerError.WithWrap(rErr).WithDebug(rErr.Error()))
        }

        return errorsx.WithStack(fosite.ErrInactiveToken.WithWrap(err).WithDebug(err.Error()))
    } else if errors.Is(err, fosite.ErrNotFound) {
        return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebugf("The refresh token has not been found: %s", err.Error()))
    } else if err != nil {
        return errorsx.WithStack(fosite.ErrServerError.WithWrap(err).WithDebug(err.Error()))
    } else if err := c.RefreshTokenStrategy.ValidateRefreshToken(ctx, originalRequest, refresh); err != nil {
        // The authorization server MUST ... validate the refresh token.
        // This needs to happen after store retrieval for the session to be hydrated properly
        if errors.Is(err, fosite.ErrTokenExpired) {
            return errorsx.WithStack(fosite.ErrInvalidGrant.WithWrap(err).WithDebug(err.Error()))
        }
        return errorsx.WithStack(fosite.ErrInvalidRequest.WithWrap(err).WithDebug(err.Error()))
    }

差分はこちらです。

あとは、リフレッシュトークンが有効期限切れのときに fosite.ErrInvalidGrant を返すことを検証するテストケースを追加しました。

Pull request は出して24時間後にはマージされ、v0.42.2 としてタグが切られています。

コントリビュートしてみて

テストコードを除けば、3行程度の小さい修正でしたがマージされると想像以上に嬉しく、エンジニアをやっていてよかったなと思う瞬間でした。
不具合の修正を待つよりも自分で修正するほうがライブラリの理解にも繋がりますし、いい経験になりました。

レビューしてくれたのは ory のCTO で、こういうコメントをもらうとまた Pull request 出したいという気持ちになるのでいいですね。

fosite をお使いの方は新しいバージョンまで上げてみてください!
fosite は ID 基盤を支える重要なライブラリの1つなので今後も継続してコントリビュートしていきたいです。