Codementor Events

Complete Oauth Flow in JavaScript Without Passport

Published Sep 21, 2020Last updated Dec 01, 2020
Complete Oauth Flow in JavaScript Without Passport

Oauth is a kind of technology that have been around since November, 2006 and it has been used widely across many applications. There are quite a number of advantages why Oauth is gaining ground as the choice way of authorizing a user. Speaking from the perspective of a client, Oauth circumvents the issue of creating passwords when trying to signup or remembering them when trying to login. Thanks to Oauth, users can now gain access to an application just by the click of a button as opposed to filling online forms which atimes can be daunting.

I recently worked on two applications (a mobile and a web application) which is fed by an express server, I wrote the code with my team and I had my slice of experience with Oauth when my stakeholders demanded it becomes a feature in the application. I will be sharing what I learnt implementing Oauth in the web application which was built with React. I will be releasing another article for the mobile, based on demand. So let us get startedstarted.jpeg

THE BACKEND IMPLEMENTATION

Experience in software development has taught me that you should never code unless you have fully understand the concept of what you are trying to achieve. Trust me, this has helped me in my profession. Oauth in plain terms is just a user requesting to be authorized via a call to an IDP(Identity Provider e.g Google, Facebook and so on). When this call is made, the IDP returns a piece of data(token or code which can be used to request for a token). With this token, we can get some user's information like email, username depending on the level of permission allowed by the user. The backend server stores this user data into the database and creates the token from it. The token we create by encoding the user information returned by the service provider is what will be used subsequently in making authorized requests.
Here is a simple illustration of the above process
Oauth implementation.png
I will be showing you the Oauth for Google but the concepts are the same irrespective of the service provider. Before you proceed, you must create an app in Google's console, you can read up how to do that here

Before we look into the code, below are the steps in creating oauth for any IDP

  • Create an app with the IDP you intend to use and get the credentials (client_id & client_secret)
  • Research to get the links for invoking the IDP's modal and also getting user information from it.
    Google:
    link for invoking modal - https://accounts.google.com/o/oauth2/v2/auth?${QUERY-PARAMS}
    link to exchange code for token - https://oauth2.googleapis.com/token
    link to get user information - https://www.googleapis.com/oauth2/v2/userinfo

LinkedIn:
link for invoking modal - https://www.linkedin.com/oauth/v2/authorization?${QUERY-PARAMS}
link to exchange code for token - https://www.linkedin.com/oauth/v2/accessToken
link to get user information (email alone) - https://api.linkedin.com/v2/clientAwareMemberHandles?q=members&projection=(elements*(primary,type,handle~))

Facebook:
link for invoking modal - https://www.facebook.com/v8.0/dialog/oauth?${QUERY-PARAMS}
link to exchange code for token - https://graph.facebook.com/v8.0/oauth/access_token
link to get user information (email alone) - https://graph.facebook.com/v8.0/me?fields=email

The concepts are basically the same irrespective of the IDP except for few changes. Just go through the documentation of the IDP you wish to implement and you will be fine. Ok, let's continue

CODE

...
/**
 * ES-6
 * @description - Oauth Google controller - a snippet just to implement the above concept
 *  
 */
 ...
const authThroughGoogle = async (req, res) => {
  try {
    const { code, redirect_uri } = req.query; // code from service provider which is appended to the frontend's URL
    const client_id = 'CLIENT_ID_FROM_APP_CREATED';
    const client_secret = 'CLIENT_SECRET_FROM_APP_CREATED';
    // The client_id and client_secret should always be private, put them in the .env file
    const grant_type = 'authorization_code'; // this tells the service provider to return a code which will be used to get a token for making requests to the service provider
    const url = 'https://oauth2.googleapis.com/token'; // link to api to exchange code for token.
    const { data } = await axios({
      url,
      method: 'POST',
      params: {
        client_id,
        client_secret,
        redirect_uri,
        code,
        grant_type,
      },
    });
  	const tokenFromGoogle = data.access_token;
    const urlForGettingUserInfo = 'https://www.googleapis.com/oauth2/v2/userinfo';
    const userData = await axios({
      urlForGettingUserInfo,
      method: 'GET',
      headers: {
        Authorization: `Bearer ${tokenFromGoogle}`,
      },
    });
    const body = {
      username: userData.data.username,
      email: userData.data.email,
      serviceProvider: 'google',
    };
    await User.create(body) // store data to database - here you can add your logic for either signing up or signing in a user. I am assuming I have a model called User, I am using Sequelize's create method to insert the user data into this model...
    const ourOwnToken = jwt.encode(body) // create token with the body variable above
    return res.status(200).json({
      success: true,
      token: ourOwnToken
    });
  } catch (err) {
    return res.status(500).json({
      success: false,
      err,
    });
  }
};
...

NB: Let us assume the route matching the above controller is: /auth/google

THE FRONTEND IMPLEMENTATION

For the frontend, we will have an anchor tag whose href's value is the location to the service provider's signin form (this is called a consent screen in Google and it is https://accounts.google.com/o/oauth2/v2/auth?{YOUR-QUERY_PARAMS}). This is a very long string, thanks to the query-params.
oauth-gif2.gif
Now, the query-params contains some very useful information let us break it down;

{

  client_id: 'CLIENT_ID_FROM_THE_APP', // It must correspond to what we declared earlier in the backend
    
    scope: "email", // This is the user data you have access to, in our case its just the mail.
    
    redirect_uri: redirectUri, // This is the uri that will be redirected to if the user signs into his google account successfully. Whatever url you specify here must be included as one of the redirect_uris in the Google app you created
    
    auth_type: "rerequest", // This tells the consent screen to reappear if the user initially entered wrong credentials into the google modal
    
    display: "popup", It pops up the consent screen when the anchor tag is clicked
    
    response_type: "code" // This tells Google to append code to the response which will be sent to the backend which exchange the code for a token

}

It is error-prone to be writing such a long url, this is why I advise to make use of an npm package called query-string which parses the url. You lay it out as a javascript object and the package parses it.

CODE

...
import * as queryString from "query-string";

const queryParams = queryString.stringify({
  client_id: 'CLIENT_ID_FROM_THE_APP', // It must correspond to what we declared earlier in the backend
    scope: "email", // This is the user data you have access to, in our case its just the mail.
    redirect_uri: redirectUri, // This is the uri that will be redirected to if the user signs into his google account successfully
    auth_type: "rerequest", // This tells the consent screen to reappear if the user initially entered wrong credentials into the google modal
    display: "popup", It pops up the consent screen when the anchor tag is clicked
    response_type: "code" // This tells Google to append code to the response which will be sent to the backend which exchange the code for a token
});
const url = `https://accounts.google.com/o/oauth2/v2/auth?${queryParams}`;
...

render () {
...
<a href={url}>Continue with Google</a>
}

When the component renders and the link is clicked, you get an image that looks like this:
Screenshot 2020-09-19 at 07.32.58.png
When you successfully sign into your Google account, you are redirected to the redirect_uri you specifed above and a new query param called code is appended to the url. Your url will look something like this now:
REDIRECT_URI_SPECIFIED_IN_APP?code=THIS IS GENERATED AND SENT BY GOOGLE AFTER SUCCESSFUL SIGNING IN

THE REDIRECT URI
At this stage, we are almost there, your redirect_uri will be of this format: {YOUR-FRONTEND-DOMAIN}/ONE-OF-THE-COMPONENT-ROUTE.Don't worry, I will explain.
Assuming I want to match the Login Component as the component the uri redirects to after successfully signing into your Google account, that means I should have some logic in this component that takes the code from the long string we talked about earlier and send to the backend which in turn send a token to the frontend that the frontend can store in storage and use to make authenticated requests.

CODE

// Do the necessary imports
...

class Login extends Component {
  socialLogin = async () => {
    	try {
          const { search } = this.props.history.location; // the search variable contains every string after the `?` mark with the `?` inclusive
          const codeFromGoogle = search.slice(6) // to get the value of the code query param.
          const res = Axios.get('BACKEND_SERVER_BASEURL/auth/google?code=codeFromGoogle&redirect_uri=FRONT_END_BASEURL/auth/google')
          if (res.data) {
            // request was successful
            localStorage.setItem('token', res.data.token) // Store the token from this request in the local storage
            this.props.history.push('/dashboard') // Log the user in and redirect to your app dashboard
          } 
        } catch (err) {
          console.error(err)
        }
    }
  componentDidMount() {
    	const {pathname} = this.props.history.location;
    	if (pathname === '/auth/google' {
        	// this ensures that the social login method is run only when the path is /auth/google
        	this.socialLogin()
        } else {
        	// the app continues with its normal logic
        }
    }
    ...

The route file should have this included:

... 
// Do the necessary imports

const Routes = () => {
  return (
    <Switch>
      <Route exact path="/login" component={Login} />
      <Route exact path="/auth/google" component={Login} />
    </Switch>
  );
};

The first path to the first route is just /login and the path to the second route is /auth/google and both are pointing to the same component (the Login component). If you look at the Login component, we have catered for this.

CONCLUSION
This is how I integrated Oauth into my web application without the use of Passportjs. I hope this will be of immense help to someone out there. Please by all means, feel free to leave your comments and questions and I will do my best to answer them. Like I said earlier, I will be releasing another article on Oauth with mobile application based on demand

Cheers and Happy Coding!!!

Discover and read more posts from Andrew Okoye
get started