TranslateProject/sources/tech/20180429 Passwordless Auth- Client.md
2018-06-16 18:15:56 +08:00

9.8 KiB
Raw Blame History

Translating by qhwdw Passwordless Auth: Client

Time to continue with the passwordless auth posts. Previously, we wrote an HTTP service in Go that provided with a passwordless authentication API. Now, we are gonna code a JavaScript client for it.

Well go with a single page application (SPA) using the technique I showed here. Read it first if you havent yet.

For the root URL (/) well show two different pages depending on the auth state: a page with an access form or a page greeting the authenticated user. Another page is for the auth callback redirect.

Serving

Ill serve the client with the same Go server, so lets add some routes to the previous main.go:

router.Handle("GET", "/js/", http.FileServer(http.Dir("static")))
router.HandleFunc("GET", "/...", serveFile("static/index.html"))

This serves files under static/js, and static/index.html is served for everything else.

You can use your own server apart, but youll have to enable CORS on the server.

HTML

Lets see that static/index.html.

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Passwordless Demo</title>
 <link rel="shortcut icon" href="data:,">
 <script src="/js/main.js" type="module"></script>
</head>
<body></body>
</html>

Single page application left all the rendering to JavaScript, so we have an empty body and a main.js file.

Ill user the Router from the last post.

Rendering

Now, create a static/js/main.js file with the following content:

import Router from 'https://unpkg.com/@nicolasparada/router'
import { isAuthenticated } from './auth.js'

const router = new Router()

router.handle('/', guard(view('home')))
router.handle('/callback', view('callback'))
router.handle(/^\//, view('not-found'))

router.install(async resultPromise => {
 document.body.innerHTML = ''
 document.body.appendChild(await resultPromise)
})

function view(name) {
 return (...args) => import(`/js/pages/${name}-page.js`)
 .then(m => m.default(...args))
}

function guard(fn1, fn2 = view('welcome')) {
 return (...args) => isAuthenticated()
 ? fn1(...args)
 : fn2(...args)
}

Differing from the last post, we implement an isAuthenticated() function and a guard() function that uses it to render one or another page. So when a user visits / it will show the home or welcome page whether the user is authenticated or not.

Auth

Now, lets write that isAuthenticated() function. Create a static/js/auth.js file with the following content:

export function getAuthUser() {
 const authUserItem = localStorage.getItem('auth_user')
 const expiresAtItem = localStorage.getItem('expires_at')

 if (authUserItem !== null && expiresAtItem !== null) {
 const expiresAt = new Date(expiresAtItem)

 if (!isNaN(expiresAt.valueOf()) && expiresAt > new Date()) {
 try {
 return JSON.parse(authUserItem)
 } catch (_) { }
 }
 }

 return null
}

export function isAuthenticated() {
 return localStorage.getItem('jwt') !== null && getAuthUser() !== null
}

When someone login, we save the JSON web token, expiration date of it and the current authenticated user on localStorage. This module uses that.

  • getAuthUser() gets the authenticated user from localStorage making sure the JSON Web Token hasnt expired yet.
  • isAuthenticated() makes use of the previous function to check whether it doesnt return null.

Fetch

Before continuing with the pages, Ill code some HTTP utilities to work with the server API.

Lets create a static/js/http.js file with the following content:

import { isAuthenticated } from './auth.js'

function get(url, headers) {
 return fetch(url, {
 headers: Object.assign(getAuthHeader(), headers),
 }).then(handleResponse)
}

function post(url, body, headers) {
 return fetch(url, {
 method: 'POST',
 headers: Object.assign(getAuthHeader(), { 'content-type': 'application/json' }, headers),
 body: JSON.stringify(body),
 }).then(handleResponse)
}

function getAuthHeader() {
 return isAuthenticated()
 ? { authorization: `Bearer ${localStorage.getItem('jwt')}` }
 : {}
}

export async function handleResponse(res) {
 const body = await res.clone().json().catch(() => res.text())
 const response = {
 url: res.url,
 statusCode: res.status,
 statusText: res.statusText,
 headers: res.headers,
 body,
 }
 if (!res.ok) throw Object.assign(
 new Error(body.message || body || res.statusText),
 response
 )
 return response
}

export default {
 get,
 post,
}

This module exports get() and post() functions. They are wrappers around the fetch API. Both functions inject an Authorization: Bearer <token_here> header to the request when the user is authenticated; that way the server can authenticate us.

Welcome Page

Lets move to the welcome page. Create a static/js/pages/welcome-page.js file with the following content:

const template = document.createElement('template')
template.innerHTML = `
 <h1>Passwordless Demo</h1>
 <h2>Access</h2>
 <form id="access-form">
 <input type="email" placeholder="Email" autofocus required>
 <button type="submit">Send Magic Link</button>
 </form>
`

export default function welcomePage() {
 const page = template.content.cloneNode(true)

 page.getElementById('access-form')
 .addEventListener('submit', onAccessFormSubmit)

 return page
}

This page uses an HTMLTemplateElement for the view. It is just a simple form to enter the users email.

To not make this boring, Ill skip error handling and just log them to console.

Now, lets code that onAccessFormSubmit() function.

import http from '../http.js'

function onAccessFormSubmit(ev) {
 ev.preventDefault()

 const form = ev.currentTarget
 const input = form.querySelector('input')
 const email = input.value

 sendMagicLink(email).catch(err => {
 console.error(err)
 if (err.statusCode === 404 && wantToCreateAccount()) {
 runCreateUserProgram(email)
 }
 })
}

function sendMagicLink(email) {
 return http.post('/api/passwordless/start', {
 email,
 redirectUri: location.origin + '/callback',
 }).then(() => {
 alert('Magic link sent. Go check your email inbox.')
 })
}

function wantToCreateAccount() {
 return prompt('No user found. Do you want to create an account?')
}

It does a POST request to /api/passwordless/start with the email and redirectUri in the body. In case it returns with 404 Not Found status code, well create a user.

function runCreateUserProgram(email) {
 const username = prompt("Enter username")
 if (username === null) return

 http.post('/api/users', { email, username })
 .then(res => res.body)
 .then(user => sendMagicLink(user.email))
 .catch(console.error)
}

The user creation program, first, ask for username and does a POST request to /api/users with the email and username in the body. On success, it sends a magic link for the user created.

Callback Page

That was all the functionality for the access form, lets move to the callback page. Create a static/js/pages/callback-page.js file with the following content:

import http from '../http.js'

const template = document.createElement('template')
template.innerHTML = `
 <h1>Authenticating you 👀</h1>
`

export default function callbackPage() {
 const page = template.content.cloneNode(true)

 const hash = location.hash.substr(1)
 const fragment = new URLSearchParams(hash)
 for (const [k, v] of fragment.entries()) {
 fragment.set(decodeURIComponent(k), decodeURIComponent(v))
 }
 const jwt = fragment.get('jwt')
 const expiresAt = fragment.get('expires_at')

 http.get('/api/auth_user', { authorization: `Bearer ${jwt}` })
 .then(res => res.body)
 .then(authUser => {
 localStorage.setItem('jwt', jwt)
 localStorage.setItem('auth_user', JSON.stringify(authUser))
 localStorage.setItem('expires_at', expiresAt)

 location.replace('/')
 })
 .catch(console.error)

 return page
}

To remember… when clicking the magic link, we go to /api/passwordless/verify_redirect which redirect us to the redirect URI we pass (/callback) with the JWT and expiration date in the URL hash.

The callback page decodes the hash from the URL to extract those parameters to do a GET request to /api/auth_user with the JWT saving all the data to localStorage. Finally, it just redirects to home.

Home Page

Create a static/pages/home-page.js file with the following content:

import { getAuthUser } from '../auth.js'

export default function homePage() {
 const authUser = getAuthUser()

 const template = document.createElement('template')
 template.innerHTML = `
 <h1>Passwordless Demo</h1>
 <p>Welcome back, ${authUser.username} 👋</p>
 <button id="logout-button">Logout</button>
 `

 const page = template.content

 page.getElementById('logout-button')
 .addEventListener('click', logout)

 return page
}

function logout() {
 localStorage.clear()
 location.reload()
}

This page greets the authenticated user and also has a logout button. The logout() function just clears localStorage and reloads the page.

There is it. I bet you already saw the demo before. Also, the source code is in the same repository.

👋👋👋


via: https://nicolasparada.netlify.com/posts/passwordless-auth-client/

作者:Nicolás Parada 选题:lujun9972 译者:译者ID 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出