From 7af35fd3cb33cd1dd96422d8123e691d93395386 Mon Sep 17 00:00:00 2001 From: Baoshan Sheng Date: Wed, 29 Sep 2021 20:36:50 +0800 Subject: [PATCH] fix: prevent `refreshToken` from lost after `resetToken` --- .github/workflows/test.yml | 2 +- README.md | 30 +++++++++++++--------- src/auth.ts | 19 ++++++++++++-- test/standalone.test.ts | 52 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 36514da..9225785 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node_version: ["12", "14"] + node_version: ["14"] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 11ff681..ecd7bf4 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,14 @@ - [Standalone usage](#standalone-usage) - [Usage with Octokit](#usage-with-octokit) - [`createOAuthUserClientAuth(options)` or `new Octokit({auth})`](#createoauthuserclientauthoptions-or-new-octokitauth) + - [Custom store](#custom-store) + - [Custom request](#custom-request) - [`auth(command)`](#authcommand) -- [Authentication object](#authentication-object) +- [Session object](#session-object) + - [Authentication object](#authentication-object) + - [OAuth APP authentication token](#oauth-app-authentication-token) + - [GitHub APP user authentication token with expiring disabled](#github-app-user-authentication-token-with-expiring-disabled) + - [GitHub APP user authentication token with expiring enabled](#github-app-user-authentication-token-with-expiring-enabled) - [`auth.hook(request, route, parameters)` or `auth.hook(request, options)`](#authhookrequest-route-parameters-or-authhookrequest-options) - [Contributing](#contributing) - [License](#license) @@ -203,17 +209,17 @@ createOAuthAppAuth({ The async `auth()` method returned by `createOAuthUserClientAuth(options)` accepts the following commands: -| Command | `{type: }` | Optional Arguments | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Sign in](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity) | `"signIn"` | | -| Get (local) token | `"getToken"` | – | -| [Create an app token](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github) | `"createToken"` | – | -| [Check a token](https://docs.github.com/en/rest/reference/apps#check-a-token) | `"checkToken"` | – | -| [Create a scoped access token](https://docs.github.com/en/rest/reference/apps#create-a-scoped-access-token) (for OAuth App) | `"createScopedToken"` | – | -| [Reset a token](https://docs.github.com/en/rest/reference/apps#reset-a-token) | `"resetToken"` | – | -| [Renewing a user token with a refresh token](https://docs.github.com/en/developers/apps/building-github-apps/reshing-user-to-server-access-tokens#renewing-a-user-token-with-a-refresh-token) (for GitHub App with token expiration enabled) | `"refreshToken"` | – | -| [Delete an app token](https://docs.github.com/en/rest/reference/apps#delete-an-app-token) (sign out) | `"deleteToken"` | `offline: true` (only deletes session from local session store) | -| [Delete an app authorization](https://docs.github.com/en/rest/reference/apps#delete-an-app-authorization) | `"deleteAuthorization"` | – | +| Command | `{type: }` | Optional Arguments | +| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Sign in](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity) | `"signIn"` | | +| Get (local) token | `"getToken"` | – | +| [Create an app token](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github) | `"createToken"` | – | +| [Check a token](https://docs.github.com/en/rest/reference/apps#check-a-token) | `"checkToken"` | – | +| [Create a scoped access token](https://docs.github.com/en/rest/reference/apps#create-a-scoped-access-token) (for OAuth App) | `"createScopedToken"` | – | +| [Reset a token](https://docs.github.com/en/rest/reference/apps#reset-a-token) | `"resetToken"` | – | +| [Renewing a user token with a refresh token](https://docs.github.com/en/developers/apps/building-github-apps/refreshing-user-to-server-access-tokens#renewing-a-user-token-with-a-refresh-token) (for GitHub App with token expiration enabled) | `"refreshToken"` | – | +| [Delete an app token](https://docs.github.com/en/rest/reference/apps#delete-an-app-token) (sign out) | `"deleteToken"` | `offline: true` (only deletes session from local session store) | +| [Delete an app authorization](https://docs.github.com/en/rest/reference/apps#delete-an-app-authorization) | `"deleteAuthorization"` | – | ## Session object diff --git a/src/auth.ts b/src/auth.ts index 4a3de49..f947b17 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -77,8 +77,10 @@ export async function auth< // Auto refresh for user-to-server token. const expiresAt = this.session.authentication.expiresAt; if (new Date(expiresAt) > new Date()) return this.session; - // @ts-ignore - return await auth.call(this, { type: "refreshToken" }); + return await auth.call(this, { type: "refreshToken" } as Command< + Client, + Expiration + >); } } @@ -118,6 +120,7 @@ export async function auth< this.session ||= await auth.call(this); if (!this.session) throw errors.unauthorized; } + const oldSession = this.session; // Prepare payload for `refreshToken` command. if (this.session && "refreshToken" in this.session.authentication) { @@ -133,6 +136,18 @@ export async function auth< this.session = response.data || null; } + // Some `oauth-app.js` endpoints (such as `resetToken`) do not (and can + // not) return `refreshToken`. Original `refreshToken` and + // `refreshTokenExpiresAt` are kept to `refreshToken` later. + if (oldSession && "refreshToken" in oldSession.authentication) { + if (this.session && !("refreshToken" in this.session.authentication)) + Object.assign(this.session.authentication, { + refreshToken: oldSession.authentication.refreshToken, + refreshTokenExpiresAt: + oldSession.authentication.refreshTokenExpiresAt, + }); + } + if (this.sessionStore) await this.sessionStore.set(this.session); return this.session; } diff --git a/test/standalone.test.ts b/test/standalone.test.ts index 6c2411c..225ac9b 100644 --- a/test/standalone.test.ts +++ b/test/standalone.test.ts @@ -663,4 +663,56 @@ describe("standalone tests under node environment", () => { expect(sessionStore.set.mock.calls.length).toEqual(1); expect(sessionStore.set.mock.calls[0][0]).toBeNull(); }); + + it("keeps refresh token", async () => { + const oldSession = { + authentication: { + token: "token123", + refreshToken: "refreshToken123", + refreshTokenExpiresAt: "2000-01-03T00:00:00.000Z", + }, + }; + const newSession = { authentication: { token: "token456" } }; + + const sessionStore = { + get: jest.fn().mockResolvedValue(oldSession), + set: jest.fn().mockResolvedValue(undefined), + }; + + const fetch = fetchMock + .sandbox() + .patchOnce("http://acme.com/api/github/oauth/token", newSession, { + headers: { + accept: "application/vnd.github.v3+json", + "user-agent": "test", + authorization: "token token123", + }, + }); + + const auth = createOAuthUserClientAuth({ + clientId: "clientId123", + sessionStore, + request: request.defaults({ + headers: { "user-agent": "test" }, + request: { fetch }, + }), + }); + + expect(await auth({ type: "resetToken" })).toEqual({ + authentication: { + token: "token456", + refreshToken: "refreshToken123", + refreshTokenExpiresAt: "2000-01-03T00:00:00.000Z", + }, + }); + expect(sessionStore.get.mock.calls.length).toBe(1); + expect(sessionStore.set.mock.calls.length).toBe(1); + expect(await auth()).toEqual({ + authentication: { + token: "token456", + refreshToken: "refreshToken123", + refreshTokenExpiresAt: "2000-01-03T00:00:00.000Z", + }, + }); + }); });