OAuth2Strategy
A strategy to use and implement OAuth2 framework for authentication with federated services like Google, Facebook, GitHub, etc.
Supported runtimes
Runtime | Has Support |
---|
Node.js | ✅ |
Cloudflare | ✅ |
Deno | ✅ |
How to use
Installation
npm add remix-auth-oauth2
![WARNING]
If you're using versions of Node.js previous to v20, you will need to make the WebCrypto API globally available to use this package.
import { webcrypto } from "node:crypto";
globalThis.crypto = webcrypto;
Or enable the experimental flag --experimental-global-webcrypto
when running your process.
For v20 or greater, this is not necessary.
Directly
You can use this strategy by adding it to your authenticator instance and configuring the correct endpoints.
export let authenticator = new Authenticator<User>(sessionStorage);
authenticator.use(
new OAuth2Strategy<
User,
{ provider: "provider-name" },
{ id_token: string }
>(
{
clientId: CLIENT_ID,
clientSecret: CLIENT_SECRET,
authorizationEndpoint: "https://provider.com/oauth2/authorize",
tokenEndpoint: "https://provider.com/oauth2/token",
redirectURI: "https://example.app/auth/callback",
tokenRevocationEndpoint: "https://provider.com/oauth2/revoke",
codeChallengeMethod: "S256",
scopes: ["openid", "email", "profile"],
authenticateWith: "request_body",
},
async ({ tokens, profile, context, request }) => {
return await getUser(tokens, profile, context, request);
},
),
"provider-name",
);
Using the Refresh Token
The strategy exposes a public refreshToken
method that you can use to refresh the access token.
let strategy = new OAuth2Strategy<User>(options, verify);
let tokens = await strategy.refreshToken(refreshToken);
The refresh token is part of the tokens
object the verify callback receives. How you store it to call strategy.refreshToken
and what you do with the tokens
object after it is up to you.
The most common approach would be to store the refresh token in the user data and then update the session after refreshing the token.
authenticator.use(
new OAuth2Strategy<User>(
options,
async ({ tokens, profile, context, request }) => {
let user = await getUser(tokens, profile, context, request);
let { access_token: accessToken, refresh_token: refreshToken } = tokens;
return { ...user, accessToken, refreshToken };
},
),
);
let user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
let session = await sessionStorage.getSession(request.headers.get("cookie"));
let tokens = await strategy.refreshToken(user.refreshToken);
session.set(authenticator.sessionKey, {
...user,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
});
Logging out the User
If you want to logout the user, aside of clearing your application session, you can revoke the access token the user has with the provider.
let user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
let tokens = await strategy.revokeToken(user.accessToken);
Extending it
You can use this strategy as a base class for another strategy using the OAuth2 framework. That way, you wouldn't need to implement the whole OAuth2 flow yourself in your custom strategy.
The OAuth2Strategy
will handle the whole flow for you and let you replace parts of it where you need.
Let's see how an Auth0Strategy
is implemented using the OAuth2Strategy
as a base.
import type { StrategyVerifyCallback } from "remix-auth";
import type {
OAuth2Profile,
OAuth2StrategyVerifyParams,
TokenResponseBody,
} from "remix-auth-oauth2";
import { OAuth2Strategy } from "remix-auth-oauth2";
export interface Auth0StrategyOptions
extends Omit<
OAuth2StrategyOptions,
"authorizationEndpoint" | "tokenEndpoint" | "tokenRevocationEndpoint"
> {
domain: string;
audience?: string;
}
export interface Auth0Profile extends OAuth2Profile {
id: string;
displayName: string;
name: {
familyName: string;
givenName: string;
middleName: string;
};
emails: Array<{ value: string }>;
photos: Array<{ value: string }>;
_json: {
sub: string;
name: string;
given_name: string;
family_name: string;
middle_name: string;
nickname: string;
preferred_username: string;
profile: string;
picture: string;
website: string;
email: string;
email_verified: boolean;
gender: string;
birthdate: string;
zoneinfo: string;
locale: string;
phone_number: string;
phone_number_verified: boolean;
address: {
country: string;
};
updated_at: string;
};
}
interface Auth0ExtraParams extends Record<string, unknown> {
id_token: string;
}
export class Auth0Strategy<User> extends OAuth2Strategy<
User,
Auth0Profile,
Auth0ExtraParams
> {
name = "auth0";
private userInfoEndpoint: string;
constructor(
{ domain, audience, ...options }: Auth0StrategyOptions,
verify: StrategyVerifyCallback<
User,
OAuth2StrategyVerifyParams<Auth0Profile, Auth0ExtraParams>
>,
) {
super(
{
authorizationEndpoint: `https://${domain}/authorize`,
tokenEndpoint: `https://${domain}/oauth/token`,
tokenRevocationEndpoint: `https://${domain}/oauth/revoke`
...options,
},
verify,
);
this.userInfoEndpoint = `https://${domain}/userinfo`;
this.audience = audience;
}
protected authorizationParams(
params: URLSearchParams,
request?: Request,
): URLSearchParams {
if (this.audience) params.set("audience", this.audience);
if (new URL(request.url).searchParams.get('example')) params.set('example', 'example');
return params;
}
protected async userProfile(
tokens: TokenResponseBody & Auth0ExtraParams,
): Promise<Auth0Profile> {
let response = await fetch(this.userInfoEndpoint, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
let data: Auth0Profile["_json"] = await response.json();
let profile: Auth0Profile = {
provider: "auth0",
displayName: data.name,
id: data.sub,
name: {
familyName: data.family_name,
givenName: data.given_name,
middleName: data.middle_name,
},
emails: [{ value: data.email }],
photos: [{ value: data.picture }],
_json: data,
};
return profile;
}
}
And that's it, thanks to the OAuth2Strategy
we don't need to implement the whole OAuth2 flow ourselves and can focus on the unique parts of our strategy which is the user profile and extra search params our provider may require us to send.