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

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

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()

}

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)

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

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"
  }
}

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

Lets create some svelte components:

touch src/Protected.svelte
touch src/Signin.svelte
touch src/Logout.svelte

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>

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
}

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)
    }
  })
})

    

Define "signin" function

In src/auth.js

const signin = () => auth2.signIn()

Define "logout" function

In src/auth.js

const logout = () => auth2.signOut()

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>

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>

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}
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

Modify rollup.config.js

...
import dotenv from 'dotenv'

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

...

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

])

create .env

CLIENT_ID=XXXXXXX

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.