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.
Top comments (0)