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

340 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.
Well go with a single page application (SPA) using the technique I showed [here][2]. 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][3] 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][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, 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][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