remix-auth-github
Version:
A strategy to implement login with GitHub in Remix Auth.
103 lines • 4.5 kB
JavaScript
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