UNPKG

remix-auth-github

Version:

A strategy to implement login with GitHub in Remix Auth.

103 lines 4.5 kB
import { Cookie, SetCookie } from "@mjackson/headers"; import { GitHub, OAuth2RequestError, UnexpectedErrorResponseBodyError, UnexpectedResponseError, generateState, } from "arctic"; import createDebug from "debug"; import { Strategy } from "remix-auth/strategy"; import { redirect } from "./lib/redirect.js"; const debug = createDebug("GitHubStrategy"); export { OAuth2RequestError, UnexpectedResponseError, UnexpectedErrorResponseBodyError, }; export class GitHubStrategy extends Strategy { options; name = "github"; client; constructor(options, verify) { super(verify); this.options = options; this.client = new GitHub(options.clientId, options.clientSecret, options.redirectURI.toString()); } get cookieName() { if (typeof this.options.cookie === "string") { return this.options.cookie || "github"; } return this.options.cookie?.name ?? "github"; } get cookieOptions() { if (typeof this.options.cookie !== "object") return {}; return this.options.cookie ?? {}; } async authenticate(request) { debug("Request URL", request.url); let url = new URL(request.url); let stateUrl = url.searchParams.get("state"); let error = url.searchParams.get("error"); if (error) { let description = url.searchParams.get("error_description"); let uri = url.searchParams.get("error_uri"); throw new OAuth2RequestError(error, description, uri, stateUrl); } if (!stateUrl) { debug("No state found in the URL, redirecting to authorization endpoint"); let state = generateState(); debug("Generated State", state); let url = this.client.createAuthorizationURL(state, this.options.scopes ?? []); url.search = this.authorizationParams(url.searchParams, request).toString(); debug("Authorization URL", url.toString()); let header = new SetCookie({ name: this.cookieName, value: new URLSearchParams({ state }).toString(), httpOnly: true, // Prevents JavaScript from accessing the cookie maxAge: 60 * 5, // 5 minutes path: "/", // Allow the cookie to be sent to any path sameSite: "Lax", // Prevents it from being sent in cross-site requests ...this.cookieOptions, }); throw redirect(url.toString(), { headers: { "Set-Cookie": header.toString() }, }); } let code = url.searchParams.get("code"); if (!code) throw new ReferenceError("Missing code in the URL"); let cookie = new Cookie(request.headers.get("cookie") ?? ""); let params = new URLSearchParams(cookie.get(this.cookieName)); if (!params.has("state")) { throw new ReferenceError("Missing state on cookie."); } if (params.get("state") !== stateUrl) { throw new RangeError("State in URL doesn't match state in cookie."); } debug("Validating authorization code"); let tokens = await this.client.validateAuthorizationCode(code); debug("Verifying the user profile"); let user = await this.verify({ request, tokens }); debug("User authenticated"); return user; } /** * Return extra parameters to be included in the authorization request. * * Some OAuth 2.0 providers allow additional, non-standard parameters to be * included when requesting authorization. Since these parameters are not * standardized by the OAuth 2.0 specification, OAuth 2.0-based authentication * strategies can override this function in order to populate these * parameters as required by the provider. */ authorizationParams(params, request) { return new URLSearchParams(params); } /** * Get a new OAuth2 Tokens object using the refresh token once the previous * access token has expired. * @param refreshToken The refresh token to use to get a new access token * @returns The new OAuth2 tokens object * @example * ```ts * let tokens = await strategy.refreshToken(refreshToken); * console.log(tokens.accessToken()); * ``` */ refreshToken(refreshToken) { return this.client.refreshAccessToken(refreshToken); } } //# sourceMappingURL=index.js.map