Skip to content

Commit 474188c

Browse files
committed
fix: prevent refreshToken from lost after resetToken
1 parent b99a8e3 commit 474188c

File tree

3 files changed

+87
-14
lines changed

3 files changed

+87
-14
lines changed

README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@
1414
- [Standalone usage](#standalone-usage)
1515
- [Usage with Octokit](#usage-with-octokit)
1616
- [`createOAuthUserClientAuth(options)` or `new Octokit({auth})`](#createoauthuserclientauthoptions-or-new-octokitauth)
17+
- [Custom store](#custom-store)
18+
- [Custom request](#custom-request)
1719
- [`auth(command)`](#authcommand)
18-
- [Authentication object](#authentication-object)
20+
- [Session object](#session-object)
21+
- [Authentication object](#authentication-object)
22+
- [OAuth APP authentication token](#oauth-app-authentication-token)
23+
- [GitHub APP user authentication token with expiring disabled](#github-app-user-authentication-token-with-expiring-disabled)
24+
- [GitHub APP user authentication token with expiring enabled](#github-app-user-authentication-token-with-expiring-enabled)
1925
- [`auth.hook(request, route, parameters)` or `auth.hook(request, options)`](#authhookrequest-route-parameters-or-authhookrequest-options)
2026
- [Contributing](#contributing)
2127
- [License](#license)
@@ -203,17 +209,17 @@ createOAuthAppAuth({
203209

204210
The async `auth()` method returned by `createOAuthUserClientAuth(options)` accepts the following commands:
205211

206-
| Command | `{type: }` | Optional Arguments |
207-
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
208-
| [Sign in](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity) | `"signIn"` | <ul><li><code>login: "user"</code></li><li><code>allowSignup: false</code></li><li><code>scopes: ["repo"]</code> (only relevant for OAuth Apps)</li></ul> |
209-
| Get (local) token | `"getToken"` ||
210-
| [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"` ||
211-
| [Check a token](https://docs.github.com/en/rest/reference/apps#check-a-token) | `"checkToken"` ||
212-
| [Create a scoped access token](https://docs.github.com/en/rest/reference/apps#create-a-scoped-access-token) (for OAuth App) | `"createScopedToken"` ||
213-
| [Reset a token](https://docs.github.com/en/rest/reference/apps#reset-a-token) | `"resetToken"` ||
214-
| [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"` ||
215-
| [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) |
216-
| [Delete an app authorization](https://docs.github.com/en/rest/reference/apps#delete-an-app-authorization) | `"deleteAuthorization"` ||
212+
| Command | `{type: }` | Optional Arguments |
213+
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- |
214+
| [Sign in](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity) | `"signIn"` | <ul><li><code>login: "user"</code></li><li><code>allowSignup: false</code></li><li><code>scopes: ["repo"]</code> (only relevant for OAuth Apps)</li></ul> |
215+
| Get (local) token | `"getToken"` ||
216+
| [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"` ||
217+
| [Check a token](https://docs.github.com/en/rest/reference/apps#check-a-token) | `"checkToken"` ||
218+
| [Create a scoped access token](https://docs.github.com/en/rest/reference/apps#create-a-scoped-access-token) (for OAuth App) | `"createScopedToken"` ||
219+
| [Reset a token](https://docs.github.com/en/rest/reference/apps#reset-a-token) | `"resetToken"` ||
220+
| [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"` ||
221+
| [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) |
222+
| [Delete an app authorization](https://docs.github.com/en/rest/reference/apps#delete-an-app-authorization) | `"deleteAuthorization"` ||
217223

218224
## Session object
219225

src/auth.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ export async function auth<
7777
// Auto refresh for user-to-server token.
7878
const expiresAt = this.session.authentication.expiresAt;
7979
if (new Date(expiresAt) > new Date()) return this.session;
80-
// @ts-ignore
81-
return await auth.call(this, { type: "refreshToken" });
80+
return await auth.call(this, { type: "refreshToken" } as Command<
81+
Client,
82+
Expiration
83+
>);
8284
}
8385
}
8486

@@ -118,6 +120,7 @@ export async function auth<
118120
this.session ||= await auth.call(this);
119121
if (!this.session) throw errors.unauthorized;
120122
}
123+
const oldSession = this.session;
121124

122125
// Prepare payload for `refreshToken` command.
123126
if (this.session && "refreshToken" in this.session.authentication) {
@@ -133,6 +136,18 @@ export async function auth<
133136
this.session = response.data || null;
134137
}
135138

139+
// Some `oauth-app.js` endpoints (such as `resetToken`) do not (and can
140+
// not) return `refreshToken`. Original `refreshToken` and
141+
// `refreshTokenExpiresAt` are kept to `refreshToken` later.
142+
if (oldSession && "refreshToken" in oldSession.authentication) {
143+
if (this.session && !("refreshToken" in this.session.authentication))
144+
Object.assign(this.session.authentication, {
145+
refreshToken: oldSession.authentication.refreshToken,
146+
refreshTokenExpiresAt:
147+
oldSession.authentication.refreshTokenExpiresAt,
148+
});
149+
}
150+
136151
if (this.sessionStore) await this.sessionStore.set(this.session);
137152
return this.session;
138153
}

test/standalone.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,4 +663,56 @@ describe("standalone tests under node environment", () => {
663663
expect(sessionStore.set.mock.calls.length).toEqual(1);
664664
expect(sessionStore.set.mock.calls[0][0]).toBeNull();
665665
});
666+
667+
it("keeps refresh token", async () => {
668+
const oldSession = {
669+
authentication: {
670+
token: "token123",
671+
refreshToken: "refreshToken123",
672+
refreshTokenExpiresAt: "2000-01-03T00:00:00.000Z",
673+
},
674+
};
675+
const newSession = { authentication: { token: "token456" } };
676+
677+
const sessionStore = {
678+
get: jest.fn().mockResolvedValue(oldSession),
679+
set: jest.fn().mockResolvedValue(undefined),
680+
};
681+
682+
const fetch = fetchMock
683+
.sandbox()
684+
.patchOnce("http://acme.com/api/github/oauth/token", newSession, {
685+
headers: {
686+
accept: "application/vnd.github.v3+json",
687+
"user-agent": "test",
688+
authorization: "token token123",
689+
},
690+
});
691+
692+
const auth = createOAuthUserClientAuth({
693+
clientId: "clientId123",
694+
sessionStore,
695+
request: request.defaults({
696+
headers: { "user-agent": "test" },
697+
request: { fetch },
698+
}),
699+
});
700+
701+
expect(await auth({ type: "resetToken" })).toEqual({
702+
authentication: {
703+
token: "token456",
704+
refreshToken: "refreshToken123",
705+
refreshTokenExpiresAt: "2000-01-03T00:00:00.000Z",
706+
},
707+
});
708+
expect(sessionStore.get.mock.calls.length).toBe(1);
709+
expect(sessionStore.set.mock.calls.length).toBe(1);
710+
expect(await auth()).toEqual({
711+
authentication: {
712+
token: "token456",
713+
refreshToken: "refreshToken123",
714+
refreshTokenExpiresAt: "2000-01-03T00:00:00.000Z",
715+
},
716+
});
717+
});
666718
});

0 commit comments

Comments
 (0)