Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ The async `auth()` method returned by `createOAuthDeviceAuth(options)` accepts t
</th>
<td>

Only relevant if the `clientType` strategy options was set to `"oauth-app"`

Array of scope names enabled for the token. Defaults to what was set in the [strategy options](#createoauthdeviceauthoptions). See <a href="https://docs.github.com/en/developers/apps/scopes-for-oauth-apps#available-scopes">available scopes</a>

</td>
Expand Down Expand Up @@ -621,10 +623,11 @@ const { data: user } = await requestWithAuth("GET /user");

```ts
import {
StrategyOptions,
AuthOptions,
Authentication,
OAuthAppStrategyOptions,
OAuthAppAuthOptions,
OAuthAppAuthentication,
GitHubAppStrategyOptions,
GitHubAppAuthOptions,
GitHubAppAuthentication,
GitHubAppAuthenticationWithExpiration,
} from "@octokit/auth-oauth-device";
Expand Down
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"author": "Gregor Martynus (https://dev.to/gr2m)",
"license": "MIT",
"dependencies": {
"@octokit/oauth-methods": "^1.1.0",
"@octokit/request": "^5.4.14",
"@octokit/request-error": "^2.0.5",
"@octokit/types": "^6.10.0",
Expand Down
30 changes: 24 additions & 6 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { getOAuthAccessToken } from "./get-oauth-access-token";
import { State, AuthOptions, Authentication, ClientType } from "./types";
import {
OAuthAppAuthOptions,
GitHubAppAuthOptions,
OAuthAppAuthentication,
GitHubAppAuthentication,
OAuthAppState,
GitHubAppState,
} from "./types";

export async function auth<TClientType extends ClientType>(
state: State,
authOptions: AuthOptions
): Promise<Authentication<TClientType>> {
return getOAuthAccessToken<TClientType>(state, {
export async function auth(
state: OAuthAppState,
authOptions: OAuthAppAuthOptions
): Promise<OAuthAppAuthentication>;

export async function auth(
state: GitHubAppState,
authOptions: GitHubAppAuthOptions
): Promise<GitHubAppAuthentication>;

export async function auth(
state: OAuthAppState | GitHubAppState,
authOptions: OAuthAppAuthOptions | GitHubAppAuthOptions
): Promise<OAuthAppAuthentication | GitHubAppAuthentication> {
// @ts-expect-error looks like TypeScript cannot handle the different OAuth App/GitHub App paths here
return getOAuthAccessToken(state, {
auth: authOptions,
});
}
99 changes: 68 additions & 31 deletions src/get-oauth-access-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,43 @@ import { RequestError } from "@octokit/request-error";
import { RequestInterface } from "@octokit/types";

import {
AuthOptions,
ClientType,
State,
Authentication,
OAuthAppState,
GitHubAppState,
OAuthAppAuthOptions,
GitHubAppAuthOptions,
OAuthAppAuthentication,
GitHubAppAuthentication,
Verification,
CodeExchangeResponseError,
ClientType,
GitHubAppAuthenticationWithExpiration,
} from "./types";

export async function getOAuthAccessToken<TClientType extends ClientType>(
state: State,
export async function getOAuthAccessToken(
state: OAuthAppState,
options: {
request?: RequestInterface;
auth: AuthOptions;
auth: OAuthAppAuthOptions;
}
): Promise<Authentication<TClientType>> {
const cachedAuthentication = getCachedAuthentication<TClientType>(
state,
options.auth
);
): Promise<OAuthAppAuthentication>;

export async function getOAuthAccessToken(
state: GitHubAppState,
options: {
request?: RequestInterface;
auth: GitHubAppAuthOptions;
}
): Promise<GitHubAppAuthentication>;

export async function getOAuthAccessToken(
state: OAuthAppState | GitHubAppState,
options: {
request?: RequestInterface;
auth: OAuthAppAuthOptions | GitHubAppAuthOptions;
}
): Promise<OAuthAppAuthentication | GitHubAppAuthentication> {
// @ts-expect-error looks like TypeScript cannot handle the different OAuth App/GitHub App paths here
const cachedAuthentication = getCachedAuthentication(state, options.auth);

if (cachedAuthentication) return cachedAuthentication;

Expand All @@ -37,7 +55,12 @@ export async function getOAuthAccessToken<TClientType extends ClientType>(
// https://docs.github.com/en/developers/apps/authorizing-oauth-apps#step-1-app-requests-the-device-and-user-verification-codes-from-github
const scope =
"scopes" in state
? { scope: (options.auth.scopes || state.scopes).join(" ") }
? {
scope: (
("scopes" in options.auth && options.auth.scopes) ||
state.scopes
).join(" "),
}
: {};
const parameters = {
baseUrl,
Expand Down Expand Up @@ -71,7 +94,7 @@ export async function getOAuthAccessToken<TClientType extends ClientType>(

// Step 3: Exchange device code for access token
// See https://docs.github.com/en/developers/apps/authorizing-oauth-apps#step-3-app-polls-github-to-check-if-the-user-authorized-the-device
const authentication = await waitForAccessToken<TClientType>(
const authentication = await waitForAccessToken(
request,
baseUrl,
state.clientId,
Expand All @@ -84,24 +107,34 @@ export async function getOAuthAccessToken<TClientType extends ClientType>(
return authentication;
}

function getCachedAuthentication<TClientType extends ClientType>(
state: State,
auth: AuthOptions
): Authentication<TClientType> | false {
function getCachedAuthentication(
state: OAuthAppState,
auth: OAuthAppAuthOptions
): OAuthAppAuthentication | false;

function getCachedAuthentication(
state: GitHubAppState,
auth: GitHubAppAuthOptions
): GitHubAppAuthentication | false;

function getCachedAuthentication(
state: OAuthAppState | GitHubAppState,
auth: OAuthAppAuthOptions | GitHubAppAuthOptions
): OAuthAppAuthentication | GitHubAppAuthentication | false {
if (auth.refresh === true) return false;
if (!state.authentication) return false;

if (state.clientType === "github-app") {
return state.authentication as Authentication<TClientType>;
return state.authentication;
}

const authentication = state.authentication as Authentication<"oauth-app">;
const newScope = (auth.scopes || state.scopes).join(" ");
const authentication = state.authentication;
const newScope = (("scopes" in auth && auth.scopes) || state.scopes).join(
" "
);
const currentScope = authentication.scopes.join(" ");

return newScope === currentScope
? (authentication as Authentication<TClientType>)
: false;
return newScope === currentScope ? authentication : false;
}

type OAuthResponseDataForOAuthApps = {
Expand Down Expand Up @@ -141,13 +174,17 @@ async function wait(seconds: number) {
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
}

async function waitForAccessToken<TClientType extends ClientType>(
async function waitForAccessToken(
request: RequestInterface,
baseUrl: string,
clientId: string,
clientType: ClientType,
verification: Verification
): Promise<Authentication<TClientType>> {
): Promise<
| OAuthAppAuthentication
| GitHubAppAuthentication
| GitHubAppAuthenticationWithExpiration
> {
const requestOptions = {
baseUrl,
method: "POST",
Expand All @@ -171,7 +208,7 @@ async function waitForAccessToken<TClientType extends ClientType>(
clientId: clientId,
token: data.access_token,
scopes: data.scope.split(/,\s*/).filter(Boolean),
} as Authentication<TClientType>;
};
}

if ("refresh_token" in data) {
Expand All @@ -189,7 +226,7 @@ async function waitForAccessToken<TClientType extends ClientType>(
apiTimeInMs,
data.refresh_token_expires_in
),
} as Authentication<TClientType>;
};
}

return {
Expand All @@ -198,12 +235,12 @@ async function waitForAccessToken<TClientType extends ClientType>(
clientType: "github-app",
clientId: clientId,
token: data.access_token,
} as Authentication<TClientType>;
};
}

if (data.error === "authorization_pending") {
await wait(verification.interval);
return waitForAccessToken<TClientType>(
return waitForAccessToken(
request,
baseUrl,
clientId,
Expand All @@ -214,7 +251,7 @@ async function waitForAccessToken<TClientType extends ClientType>(

if (data.error === "slow_down") {
await wait(verification.interval + 5);
return waitForAccessToken<TClientType>(
return waitForAccessToken(
request,
baseUrl,
clientId,
Expand Down
9 changes: 5 additions & 4 deletions src/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
} from "@octokit/types";

import { getOAuthAccessToken } from "./get-oauth-access-token";
import { ClientType, State } from "./types";
import { OAuthAppState, GitHubAppState } from "./types";
import { EndpointDefaults } from "@octokit/types";

export async function hook<TClientType extends ClientType>(
state: State,
export async function hook(
state: OAuthAppState | GitHubAppState,
request: RequestInterface,
route: Route | EndpointOptions,
parameters?: RequestParameters
Expand All @@ -26,7 +26,8 @@ export async function hook<TClientType extends ClientType>(
return request(endpoint);
}

const { token } = await getOAuthAccessToken<TClientType>(state, {
// @ts-expect-error looks like TypeScript cannot handle the different OAuth App/GitHub App paths here
const { token } = await getOAuthAccessToken(state, {
request,
auth: { type: "oauth" },
});
Expand Down
Loading