DEV Community

hyper
hyper

Posted on • Originally published at hyper63.Medium on

Svelte + Google OAuth2

Authentication is a PAIN, there are so many decisions, functional and non functional requirements to work through. But suppose, just suppose, that you only had the following requirements:

Requirements

  • Internal portal for a company using GSuite or GMail
  • A single role user that can perform every feature of the application

Now, this is a contrived scenario, but it will keep this tutorial simple and you can derive something more complex from this tutorial in the future.

Google Authentication Setup

In order to follow this tutorial you will need to setup some authorization credentials. https://developers.google.com/identity/sign-in/web/sign-in

Make sure you copy your CLIENT_ID

Technical Stack

  • Svelte for the Web App * Svelte-Query — Data management * Tinro — Routing
  • NodeJS for the API service * express framework

Getting started

Lets create a project folder called auth-example and in this project folder lets create two sub-project folders:

mkdir api

npx degit sveltejs/template app
Enter fullscreen mode Exit fullscreen mode

This will give us api and app - lets start on the api side.

Setup Express Server

cd api
yarn init -y
yarn add express google-auth-library ramda crocks cors
yarn add nodemon dotenv -D
touch server.js
touch verify.js
Enter fullscreen mode Exit fullscreen mode

edit verify.js

Verify will be our middleware, this middleware will verify every request to our API server.

import { default as google } from 'google-auth-library'
import { default as R } from 'ramda'
import { default as crocks } from 'crocks'

const { Identity } = crocks
const { pathOr, split, nth } = R

export default (CLIENT_ID) => (req, res, next) => {
  const client = new google.OAuth2Client(CLIENT_ID)
  return client.verifyIdToken({
    idToken: extractToken(req),
    audience: CLIENT_ID
  })
  .then(ticket => req.user = ticket.getPayload())
  .then(() => next())
  .catch(error => next(error))

}

function extractToken(req) {
  return Identity(req)
    .map(pathOr('Bearer INVALID', ['headers', 'authorization']))
    .map(split(' '))
    .map(nth(-1))
    .valueOf()

}
Enter fullscreen mode Exit fullscreen mode

Here we are initializing the OAuth2 Client and then verifying the Bearer Token from the request with GoogleAuth, if successful we get the user profile, if not we get an error which will be handled downstream.

edit server.js

import express from 'express'
import verify from './verify.js'
import cors from 'cors'

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

app.use(verify(process.env.CLIENT_ID))

app.use((error, req, res, next) => {
  console.log('handle error')
  res.status(500).json({ok: false, message: error.message})
})

app.get('/api', (req, res) => {
  console.log(req.user)
  res.json(req.user)
})

app.listen(4000)
Enter fullscreen mode Exit fullscreen mode

In our server.js we use our verify middleware passing in the CLIENT_ID and we setup an error handler and a single /api endpoint. Finally, we listen on port 4000.

Create a .env file and store your Google Auth Client Credentials in this file:

CLIENT_ID=XXXXXXXXXXXXXXXXX
Enter fullscreen mode Exit fullscreen mode

Update your package.json

{
  "name": "api",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "dependencies": {
    "cors": "^2.8.5",
    "crocks": "^0.12.4",
    "express": "^4.17.1",
    "google-auth-library": "^7.0.2",
    "ramda": "^0.27.1"
  },
  "devDependencies": {
    "@swc-node/register": "^1.0.5",
    "@types/express": "^4.17.11",
    "@types/node": "^14.14.35",
    "dotenv": "^8.2.0",
    "nodemon": "^2.0.7",
    "typescript": "^4.2.3"
  },
  "scripts": {
    "dev": "node -r dotenv/config server.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

And you can start your server using yarn dev

Setting up the App

Now that we have our server running, we need to open a new terminal window to our project directory and cd in the app directory.

cd app
yarn
Enter fullscreen mode Exit fullscreen mode

Lets create some svelte components:

touch src/Protected.svelte
touch src/Signin.svelte
touch src/Logout.svelte
Enter fullscreen mode Exit fullscreen mode

Adding Google Auth Loaders

The Google Auth loading scripts need to be added to the index.html page so that they are available in our application.

edit public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset='utf-8'>
    <meta name='viewport' content='width=device-width,initial-scale=1'>
    <meta name='google-signin-client_id' content="YOUR CLIENT ID HERE">

    <title>Svelte Auth Test app</title>

    <link rel='icon' type='image/png' href='/favicon.png'>
    <link rel='stylesheet' href='/global.css'>
    <link rel='stylesheet' href='/build/bundle.css'>
    <script src="https://apis.google.com/js/platform.js"></script>
    <script defer src='/build/bundle.js'></script>
</head>

<body>
  <div id="app">
  </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Creating an auth store

In Svelte, we can have these reactive modules called stores, they have a subscribe, set and update methods. The Svelte compiler can recognize these stores and manage reactive events.

We want our store to expose a user store that has the following properties:

  • subscribe
  • signin
  • logout

src/auth.js

import { writable } from 'svelte/store'

var auth2
var googleUser

const { subscribe, set, update } = writable(null)

export const user = {
  subscribe,
  signin,
  logout
}
Enter fullscreen mode Exit fullscreen mode

Initializing Google Auth

In our auth.js file, we want to initialize the google auth api.

gapi.load('auth2', () => {
  auth2 = gapi.auth2.init({
    client_id: __CLIENT_ID__ ,
    scope: 'profile'
  })

  auth2.isSignedIn.listen((loggedIn) => {
    if (loggedIn) {
      const u = auth2.currentUser.get()
      const profile = u.getBasicProfile()
      update(() => ({
        profile: {
          id: profile.getId(),
          name: profile.getName(),
          image: profile.getImageUrl(),
          email: profile.getEmail()
        },
        token: u.getAuthResponse().id_token
      })
    } else {
      update(() => null)
    }
  })
})
Enter fullscreen mode Exit fullscreen mode

Define “signin” function

In src/auth.js

const signin = () => auth2.signIn()
Enter fullscreen mode Exit fullscreen mode

Define “logout” function

In src/auth.js

const logout = () => auth2.signOut()
Enter fullscreen mode Exit fullscreen mode

Setting up our App Component

In our App.svelte file, we want to import the Route component and a SignIn and Logout component buttons.

<script>
import { Route } from 'tinro'
import { user } from './auth.js'

import Protected from './Protected.svelte'

//...
</script>
Enter fullscreen mode Exit fullscreen mode

Some simple markup

<h1>Google Auth</h1>
{#if $user}
  <button on:click={() => { user.logout(); router.goto('/'); }}>Logout</button>
{:else}
  <button on:click={() => user.signin()}>Sign In</button>
{/if}

<Route path="/">
  {#if $user}

  <img src={$user.profile.image} alt={$user.profile.name} />
  <p>Welcome {$user.profile.name}</p>
  <div>
  <a href="/protected">Protected</a>
  </div>
  {/if}
  <hr />
  {JSON.stringify($user, null, 2)}
</Route>
<Route path="/protected">
  <Home /> 
</Route>
Enter fullscreen mode Exit fullscreen mode

Protected Component

src/Protected.svelte

<script>
  import { user } from './auth.js'

  const getProfile = () => new Promise(function (resolve, reject) {
    user.subscribe(u => {
      if (u) {
        fetch('http://localhost:4000/api', { headers: { Authorization: `Bearer ${u.token}`}})
          .then(res => res.json())
          .then(result => resolve(result))
          .catch(e => reject(e))
      }
    })

  })


</script>
<h1>Protected Page</h1>
{#await getProfile()}
  <p>Loading...</p>
{:then profile}
  <h2>{profile.name}</h2>
  <p>sub: {profile.sub}</p>
  <p>email: {profile.email}</p>
  <p>domain: {profile.hd}</p>
  <a href="/">Home</a>
{:catch error}
  <div>Not Authorized!</div>
  <a href="/">Home</a>
{/await}
Enter fullscreen mode Exit fullscreen mode

Note: Replace the __CLIENT_ID__ with the google client id using the rollup replace plugin.

Install dotenv and @rollup/plugin-replace

yarn add -D @rollup/plugin-replace dotenv
Enter fullscreen mode Exit fullscreen mode

Modify rollup.config.js

...
import dotenv from 'dotenv'

if (!production) { dotenv.config() }

...

plugins([
  replace({
    __CLIENT_ID__ : process.env.CLIENT_ID
  }),
  ...

])
Enter fullscreen mode Exit fullscreen mode

create .env

CLIENT_ID=XXXXXXX
Enter fullscreen mode Exit fullscreen mode

Summary

This post is a companion post to the screencast, you can use it as notes to follow along with the video. Authentication is hard, even with tools like Google OAuth, and JWTs. Hopefully, this screencast and notes gives you a way to get started using authentication in Svelte.

Top comments (0)