Translating by qhwdw Passwordless Auth: Client ====== Time to continue with the [passwordless auth][1] 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. We’ll go with a single page application (SPA) using the technique I showed [here][2]. Read it first if you haven’t yet. For the root URL (`/`) we’ll 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 I’ll serve the client with the same Go server, so let’s 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 you’ll have to enable [CORS][3] on the server. ### HTML Let’s see that `static/index.html`. ``` Passwordless Demo ``` Single page application left all the rendering to JavaScript, so we have an empty body and a `main.js` file. I’ll user the Router from the [last post][2]. ### 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, let’s 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 hasn’t expired yet. * `isAuthenticated()` makes use of the previous function to check whether it doesn’t return `null`. ### Fetch Before continuing with the pages, I’ll code some HTTP utilities to work with the server API. Let’s 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 ` header to the request when the user is authenticated; that way the server can authenticate us. ### Welcome Page Let’s 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 = `

Passwordless Demo

Access

` 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 user’s email. To not make this boring, I’ll skip error handling and just log them to console. Now, let’s 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, we’ll 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, let’s 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 = `

Authenticating you 👀

` 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 = `

Passwordless Demo

Welcome back, ${authUser.username} 👋

` 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][4] before. Also, the source code is in the same [repository][5]. 👋👋👋 -------------------------------------------------------------------------------- via: https://nicolasparada.netlify.com/posts/passwordless-auth-client/ 作者:[Nicolás Parada][a] 选题:[lujun9972](https://github.com/lujun9972) 译者:[译者ID](https://github.com/译者ID) 校对:[校对者ID](https://github.com/校对者ID) 本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 [a]:https://nicolasparada.netlify.com/ [1]:https://nicolasparada.netlify.com/posts/passwordless-auth-server/ [2]:https://nicolasparada.netlify.com/posts/javascript-client-router/ [3]:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS [4]:https://go-passwordless-demo.herokuapp.com/ [5]:https://github.com/nicolasparada/go-passwordless-demo