Skip to content

Instantly share code, notes, and snippets.

@chanan
Created December 21, 2021 21:55
Show Gist options
  • Save chanan/d1601c63df36ac476e84fbc2ca96b2e9 to your computer and use it in GitHub Desktop.
Save chanan/d1601c63df36ac476e84fbc2ca96b2e9 to your computer and use it in GitHub Desktop.
Cognito authentication method for Remix
import { redirect, createCookie } from "remix";
const sessionSecret = process.env.SESSION_SECRET;
const cognitoDomain = process.env.COGNITO_DOMAIN;
const clientId = process.env.CLIENT_ID;
if (!sessionSecret) {
throw new Error("SESSION_SECRET must be set");
}
if (!cognitoDomain) {
throw new Error("COGNITO_DOMAIN must be set");
}
if (!clientId) {
throw new Error("CLIENT_ID must be set");
}
const cookieSettings = {
maxAge: 60 * 60 * 30,
secure: process.env.NODE_ENV === "production",
secrets: [sessionSecret],
httpOnly: true
}
const cookieAccessToken = createCookie("cognitoAccessToken", cookieSettings);
const cookieIdToken = createCookie("cognitoIdToken", cookieSettings);
const cookieRefreshToken = createCookie("cognitoRefreshToken", cookieSettings);
export async function authenticate(request) {
const url = new URL(request.url);
const redirectUri = url.origin + url.pathname;
const code = url.searchParams.get("code");
const redirectTo = encodeURIComponent(url.searchParams.get("redirectTo") || "/");
const headers = new Headers();
let user = null;
if (code) {
//If the url has a code, we redirected the user to the cognito and they were authenticated
const tokenResponse = await getToken(code, redirectUri);
if (tokenResponse.status === 200) {
const json = await tokenResponse.json();
const { access_token, id_token, refresh_token } = json;
user = await getUser(access_token);
headers.append("Set-cookie", await cookieAccessToken.serialize({
access_token
}));
headers.append("Set-cookie", await cookieIdToken.serialize({
id_token
}));
headers.append("Set-cookie", await cookieRefreshToken.serialize({
refresh_token
}));
}
}
//The url does not have a code, so this is the first time we are hitting the login page
//First try to get a user from an access token saved as a cookie
if (!user) {
user = await hasValidAccessToken(request);
if (!user) {
//Then try to refresh the access token from a refresh token saved as a cookie
const { accessToken, idToken, refreshToken } = await refreshAccessToken(request, redirectUri);
if (accessToken) {
user = await getUser(accessToken);
if (user) {
headers.append("Set-cookie", await cookieAccessToken.serialize({
access_token: accessToken
}));
headers.append("Set-cookie", await cookieIdToken.serialize({
id_token: idToken
}));
headers.append("Set-cookie", await cookieRefreshToken.serialize({
refresh_token: refreshToken
}));
}
}
}
if (!user) {
//if we still have no user then send them to the cognito login page
const uri = `https://${cognitoDomain}/login?client_id=${clientId}&response_type=code&scope=email+openid&redirect_uri=${redirectUri}&state=${redirectTo}`;
return redirect(uri);
}
}
if (user) {
//TODO Persist the user in the session
console.log("This should be persisted in session: ", user);
const state = url.searchParams.get("state");
const finalRedirectTo = decodeURIComponent(state || redirectTo);
console.log('finalRedirectTo :>> ', finalRedirectTo);
return redirect(finalRedirectTo, { headers });
}
//All failed, return to login
return redirect(`/login?redirect=${redirectTo}`);
}
//Make the call to cognito to get the token
async function getToken(code, redirectUri) {
const uri = `https://${cognitoDomain}/oauth2/token`;
const body = {
grant_type: "authorization_code",
client_id: clientId,
redirect_uri: redirectUri,
code
}
return response = await fetch(uri, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(body)
});
}
//Get the user info. If this call succeeds, the user is authenticated
async function getUser(access_token) {
const uri = `https://${cognitoDomain}/oauth2/userInfo`;
const response = await fetch(uri, {
method: "GET",
headers: {
"Authorization": `Bearer ${access_token}`
},
});
if (response.status === 200) {
return await response.json();
} else {
return null;
}
}
//Does the user have a valid access token? If so, return the user info
async function hasValidAccessToken(request) {
const cookieHeaders = request.headers.get("Cookie");
if (cookieHeaders) {
const cookieAccessTokenValue = await (cookieAccessToken.parse(cookieHeaders) || {});
if (cookieAccessTokenValue.access_token) {
return await getUser(cookieAccessTokenValue.access_token);
}
}
return null;
}
async function refreshAccessToken(request, redirectUri) {
const ret = {
accessToken: undefined,
idToken: undefined,
refreshToken: undefined
}
const cookieHeaders = request.headers.get("Cookie");
if (cookieHeaders) {
const cookieRefreshTokenValue = await (cookieRefreshToken.parse(cookieHeaders) || {});
if (cookieRefreshTokenValue.refresh_token) {
const uri = `https://${cognitoDomain}/oauth2/token`;
const body = {
grant_type: "refresh_token",
client_id: clientId,
redirect_uri: redirectUri,
refresh_token: cookieRefreshTokenValue.refresh_token
}
const response = await fetch(uri, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(body)
});
if (response.status === 200) {
const json = await response.json();
const { access_token, id_token, refresh_token } = json;
ret.accessToken = access_token;
ret.idToken = id_token;
ret.refreshToken = refresh_token;
}
}
}
return ret;
}
@chanan
Copy link
Author

chanan commented Dec 24, 2021

@misantronic Session secret is an environment variable that is used to encrypt the cookie sent to the user.

Also used the in Jokes App Tutorial in this section where it talks about session.server.ts:

https://remix.run/docs/en/v1/tutorials/jokes#build-the-login-form

@cjoecker
Copy link

Is there an specific reason why not to use remix-auth with the OAuth2Strategy ?

@Visible-Radio
Copy link

Shouldn't the code exchange request include the client secret?

@chanan
Copy link
Author

chanan commented Feb 13, 2024

This code is pretty old, I am not sure its good. Some things to keep in mind. First Remix now has a good auth library that you might be better off using: https://github.com/sergiodxa/remix-auth

Second even if you do use this gist, you can imporve it by checking the JWT token locally rather than hitting /userInfo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment