Add Social Authentication to Remix Run Project | Express Server
Social Login is a form of single sign-on using existing information from a social network provider like Google, Facebook, Twitter, Github, and so on.
If your app server is Remix Server then you should check out this blog-post where we added Social Auth when our server is Remix Server. In this blog post, we would talk about how we can integrate Login via Google
with Remix Run project when our server is Express Server!
Setting up an app in Google Developer Console and getting GOOGLE_CLIENT_ID and GOOLGE_CLIENT_SECRET
Before we start any development, first we need to set up an app in Google Developer Console and get the necessary credentials.
- Go to https://console.cloud.google.com/
- Select a project or create a new one
- Once the project is selected, go to https://console.cloud.google.com/apis/credentials
- Open the Credentials tab from the sidebar
- Click on
Create Credentials
and selectOAuth client ID
- Select Application type as
Web application
. - Name your application. For e.g.
LoginApp
- In the
Authorised redirect URIs
section, click onADD URI
and addhttp://localhost:3000/auth/google/callback
- Click on
CREATE
- You will now get your
GOOGLE_CLIENT_ID
andGOOGLE_CLIENT_SECRET
. Copy and save them securely.
Create a Remix Run Project
We are going to use passport.js to provide authentication functionality. It is a middleware for Node.js and works well with Express-based web applications. So, we are going to create a fresh Remix project and choose Express as our server.
npx create-remix@latest
# choose Express Server
cd [whatever you named the project]
Load Secrets as ENVIRONMENT VARIABLES
- Let us first create a
.env
file to store theGOOGLE_CLIENT_ID
andGOOGLE_CLIENT_SECRET
downloaded earlier.
touch .env
- Install the
dotenv
package to load these variables into our app.
npm i dotenv
- Update our Server File
// file: server/index.js
// this loads the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` as environment variables
// that can be accessed via
// process.env.GOOGLE_CLIENT_ID
// process.env.GOOGLE_CLIENT_SECRET
require("dotenv").config();
Install Dependencies
Let us install the required packages from npm.
PassportJS
passport-google-oauth2
- This is the Google Strategy Library that we are going to use to provideLogin with Google
functionality.
npm i passport passport-google-oauth2
Initializing Passport
// file: server/index.js;
const passport = require("passport");
app.use(passport.initialize());
Configuring Strategy
// file: server/index.js
...
const GoogleStrategy = require("passport-google-oauth2").Strategy;
async function handleStrategyCallback(request, accessToken, refreshToken, profile, done) {
// Use profile data and token to create a user profile in your system
// profile object will have data like
// id, emails, displayName, photos, provider, profileUrl
// Something like
const user = await User.findOrCreate({ googleId: profile.id });
// once user is stored in your db
return done(null, user);
}
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:3000/auth/google/callback", // change the domain to your domain in production
scope: ["openid email profile"],
passReqToCallback: true,
},
handleStrategyCallback
)
);
...
Implementing serializeUser() and deserializeUser()
Passport uses the serializeUser
function to persist user data (after successful authentication) and function deserializeUser
is used to retrieve the user data.
// file: server/index.js;
// user object is the authenticated user that we get from `handleStrategyCallback` function
passport.serializeUser((user, done) => {
done(null, user);
});
// user object is the authenticated user that we get from `serializeUser` function
passport.deserializeUser((user, done) => {
done(null, user);
});
Setting up request URLs
First, we are going to handle the route when the request initially comes to our backend i.e. when the user clicks on the Login with Google
button then call a is made to our back-end. It would be the starting point for the social auth flow.
// file: server/index.js
app.use("/auth/google/", (req, res, next) => {
// invoke passport authentication function with google strategy
return passport.authenticate("google", {
failureRedirect: "/error",
})(req, res, next);
});
Now, the user would be redirected to the Google Sign-in screen where they can log in to their account and allow our app. Once that is done then our handleStrategyCallback
function would be invoked with all the necessary information provided by Google and we save the user to our database. A call is made to the Authorised URI
we provided during our initial project setup.
app.use("/auth/google/callback", (req, res, next) => {
// callback from Google
return passport.authenticate("google", {
failureRedirect: "/error",
})(req, res, next);
});
Handling User Session
- Since, we are using an Express server, we can manage the user session on our own using packages like express session. If we go that route then we have to make some changes to let passport manage the session.
// file: server/index.js
const session = require("express-session");
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
})
);
app.use(passport.session());
Now, we will have req.session.passort.user
that will contain our authenticated user object. We can use this information to perform check on routes access.
- We can use Remix's built-in session management utilities to manage the session.
Remix Server Side Session Handling
Let us create session handling using Remix built-in utilities.
cd app
touch session.server.js
// file: app/session.server.js
import { createCookieSessionStorage, redirect } from "remix";
// loads this from .env file in production
const sessionSecret = "secret";
const storage = createCookieSessionStorage({
cookie: {
name: "cookie_name",
secure: true,
secrets: [sessionSecret],
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
},
});
export async function createUserSession(userId, redirectTo) {
const session = await storage.getSession();
session.set("userId", userId);
return redirect(redirectTo, {
headers: {
"Set-Cookie": await storage.commitSession(session),
},
});
}
In the above code snippet, we created a function createUserSession
that takes userId
as a parameter and creates the session for us. However, we don't have userId. We need to pass that userId from our express route handling to Remix securely.
We can do so by creating an API route in Remix server code and passing the userId
to it as a cookie from our /auth/google/callback
request. To securely pass it, we will use a package string-crypto to encrypt and decrypt the cookie value.
npm i string-crypto
const StringCrypto = require("string-crypto");
const { encryptString } = new StringCrypto();
function handleSocialLoginCallback(req, res, next) {
// change the secret to something more secure
// store it as env variable
const encryptedUserId = encryptString(req.user.userId, "secret");
res.cookie("cookie-name", encryptedUserId, {
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
});
return res.redirect(`http://localhost:3000/api/users/auth`);
}
app.use(
"/auth/google/callback",
(req, res, next) => {
// callback from Google
return passport.authenticate("google", {
failureRedirect: "/error",
})(req, res, next);
},
handleSocialLoginCallback
);
Create Remix Route and session creation
cd app/routes
touch api.users.auth.js
// file: api.users.auth.js
import { redirect } from "remix";
import StringCrypto from "string-crypto";
import { createUserSession } from "../app/session.server";
const { decryptString } = new StringCrypto();
export const loader = async ({ request }) => {
const cookies = request.headers.get("Cookie") || "";
const cookiesArray = cookies.split(";");
const userCookie = cookiesArray.find((cookie) =>
cookie.includes("cookie-name")
);
if (!userCookie) {
return redirect("/error");
}
let encryptedUserId = userCookie.split("=")[1];
encryptedUserId = decodeURIComponent(userId);
const userId = decryptString(encryptedUserId, "secret");
return await createUserSession(userId, "/dashboard");
};
In the above code snippet, we read the cookie from the request headers. Decoded it using the secret, created the user session, and redirected the logged-in user to the dashboard.
Handling Logout
Let us add a function to logout users that delete the session using the storage
object we created earlier.
// file: app/session.server
...
export async function logout(request) {
const session = await storage.getSession(request.headers.get("Cookie"));
return redirect("/login", {
headers: {
"Set-Cookie": await storage.destroySession(session),
},
});
}
Now, when we receive a request to GET /logout
then we can call the above-created function and delete the user session.
See the entire flow live here -- https://devtools.tech/login
I hope this blog post helped you in some way. Please do share it and show our content much needed love! :D
If you feel something is missing/wrong/improvement is possible then feel free to reach out -- Devtools Tech and Yomesh Gupta