DEV Community

Kuvam Bhardwaj
Kuvam Bhardwaj

Posted on

Adding LinkedIn OAuth to web applications (without external libraries)

Introduction

A recent project got me into LinkedIn Oauth, And boy it was a headache at first! And today i'll try to save you from that headache. So, lets get started!

Setup

We'll be needing a linkedin app which would represent our web app for authentication to the user (create the linkedin app from here). We also need a page for our app... (I know, this sh*t takes ages 😑)
linkedin auth page

After creating a page with minimal details and pasting the page url in the input box, we are ALMOST done creating the app
linkedin auth app

Now if we see our app on the url: https://www.linkedin.com/developers/apps, it should contain a section named "Auth"
linkedin app auth section

Going in the Auth section, we will be shown with some INTERESTING stuff. Here, we need to provide a redirect uri to the auth app.
linkedin auth setup almost complete

*Note: Redirect uri is the absolute url path of the part of your web app on which you want linkedin to redirect to after authentication.

Here, i am hosting the web app on my local machine so i am giving the url http://localhost:3000, you may give any other route or url.
I suggest to store the redirect uri in an environment variable in your app as we will be requiring it quite often and also env variables are suggested way to organise fundamental constant variables which allow us to use them in different environments with different values (i.e production & development).

Now, coming to the final setup step. We haven't given any scopes/permissions for oauth to our app as we can see here in the Auth tab.
linkedin oauth permissions

To give permissions to our app, we need to go in Products tab > Sign In with LinkedIn and click on Select > Add Product. LinkedIn will review our app and after a few moments, the product should be added to it (this sh*t takes ages 😑). Once complete, it will be reflected in the "Auth" tab
linkedin auth scopes
Phew! that was a lot of setup! lets move on to the fun part ;)

Authentication Flow

LinkedIn auth has a 3-step authentication process:

  1. Get the authorization code (done on frontend)
  2. Exchange the code to get an access_token (requires backend)
  3. Exchange the access_token to get user's details (email, name etc.) (requires backend)

What I mean by requiring a backend?

Responses from the requests to be made in the step 2 & 3 don't have an Access-Control-Allow-Origin header. Which means that we can't read the data sent back in response by linkedin's servers as the browser blocks these type of requests, you can see more on this header here.
Thus, we need something which is not running on browser.

Code

Step 1 (Fetching the authorization_code)

To get the authorization code, we need to redirect the user to this url with the following parameters:
linkedin client redirect url

(the state parameter is not required and neither we will be using it)
Your final url to redirect user on, should look like this:

`https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${process.env.REACT_APP_CLIENTID}&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&scope=r_liteprofile,r_emailaddress`
Enter fullscreen mode Exit fullscreen mode

Now, lets move on to the frontend where we will be fetching the authorization_code, i am using React here but you could use pure vanillaJS too.

// App.js
export default function App() {

  const linkedinRedirectUrl = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${process.env.REACT_APP_CLIENTID}&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&scope=r_liteprofile,r_emailaddress`

  const handleLinkedinLogin = () => {
    window.location.href = linkedinRedirectUrl
  }

  return (
    <div className="App">
      <button onClick={handleLinkedinLogin}>
        Login with LinkedIn
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Try clicking the button...
it works

IT WORKS!

Clicking on Allow will redirect to home page of our app, but in the address bar there's something different!
linkedin auth code

Thats THE CODE!

Step 2 (Retrieving the authorization_code)

Now, in this step we must retrieve the code after the redirection so that we could perform operations on it. So i have written a useEffect to retrieve the code if it is present in address bar on every page load.

// App.js
import { useEffect } from 'react'

export default function App() {

  const linkedinRedirectUrl = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${process.env.REACT_APP_CLIENTID}&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&scope=r_liteprofile,r_emailaddress`

  useEffect(() => {
    let windowUrl = window.location.href
    if (windowUrl.includes('code=')) {
      let codeMatch = windowUrl.match(/code=([a-zA-Z0-9_\-]+)/)
      // And to those who hate regex...
      // YES I used regex here! B*TCHES!
      // Anyway, I prefer learning it, quite POWERFUL as shown

    }

  }, [])

  const handleLinkedinLogin = () => {
    window.location.href = linkedinRedirectUrl
  }

  return (
    <div className="App">
      <button onClick={handleLinkedinLogin}>
        Login with LinkedIn
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

About that regex i used, it just means to pickup a group of characters which are lowercase alphabets (a-z), uppercase alphabets (A-Z), underscores & hyphens. Reloading the page now extracts the code.
Now we need a backend to request the access_token exchanging this code and request user info then & there with granted access_token. Lets build that!

Step 3 (fetching the user_info with access_token)

Here's the backend code for the endpoint which will fetch the access_token & user info.

require('dotenv').config()
const cors = require('cors')
const axios = require('axios')

const app = require('express')()
app.use(cors())

app.get('/user', async (req, res) => {
    try {
        const code = req.headers.auth_code
        if (!code) throw new Error('No code provided')

        // This request gets access_token
        let accessTokenResponse = await axios.get(`https://www.linkedin.com/oauth/v2/accessToken?grant_type=authorization_code&code=${code}&client_id=${process.env.CLIENTID}&client_secret=${process.env.CLIENT_SECRET}&redirect_uri=${process.env.REDIRECT_URI}`)

        // This request gets user info from access_token (given in the headers of the request)
        let userInfoResponse = await axios.get('https://api.linkedin.com/v2/me', {
            headers: {
                'Authorization': `Bearer ${accessTokenResponse.data.access_token}`
            }
        })

        return res.status(200).json(userInfoResponse.data)

    } catch (err) {
        console.log(err)
        return res.status(400).json({ message: 'Error authenticating' })

    }
})

app.listen(3001, () => console.log('Server started'))
Enter fullscreen mode Exit fullscreen mode

Endpoint all setup! lets add the block to make a GET request to this endpoint in the useEffect of our frontend.

// App.js
import axios from 'axios'
import { useEffect } from 'react'

export default function App() {

  const linkedinRedirectUrl = `https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=${process.env.REACT_APP_CLIENTID}&redirect_uri=${process.env.REACT_APP_REDIRECT_URI}&scope=r_liteprofile,r_emailaddress`

  useEffect(() => {
    let windowUrl = window.location.href
    if (windowUrl.includes('code=')) {
      let codeMatch = windowUrl.match(/code=([a-zA-Z0-9_\-]+)/)

      axios.get('http://localhost:3001/user', {
        headers: {
          auth_code: codeMatch[1]
        }
      })
      .then(res => {
        console.log(res.data)

      })
      .catch(console.log)

    }

  }, [])

  const handleLinkedinLogin = () => {
    window.location.href = linkedinRedirectUrl
  }

  return (
    <div className="App">
      <button onClick={handleLinkedinLogin}>
        Login with LinkedIn
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

*Tip: Try printing the codeMatch variable to get idea on whats going on in the .match() method

Now, lets once more try clicking the "Login with LinkedIn" button...
badabing badaboom linked auth success

success

Top comments (1)

Collapse
 
pavel_rogovoy_a25bcb4ec4f profile image
Pavel Rogovoy • Edited

Finally a great tutorial! Thank you for the effort!!

By the way - it is important to note that not using "state" parameter exposes you to csrf attack.