From ae77a74de83be3501d568e86c9fe94d85654420c Mon Sep 17 00:00:00 2001 From: darksun Date: Fri, 15 Jun 2018 17:45:17 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=89=E9=A2=98:=20Passwordless=20Auth:=20Cl?= =?UTF-8?q?ient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20180429 Passwordless Auth- Client.md | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 sources/tech/20180429 Passwordless Auth- Client.md diff --git a/sources/tech/20180429 Passwordless Auth- Client.md b/sources/tech/20180429 Passwordless Auth- Client.md new file mode 100644 index 0000000000..06e4464709 --- /dev/null +++ b/sources/tech/20180429 Passwordless Auth- Client.md @@ -0,0 +1,338 @@ +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