YoutubeのfreeCodeCamp.orgでJWTについて動画があったのでまとめます。自分用に(笑。
jsonwebtokenの使い方について https://www.npmjs.com/package/jsonwebtoken。
この動画の中でserver側とfront側で分けていたのでそれに習います。
server側では
cors と cookie-parserを使ってログイン情報を受け取ります。
https://www.npmjs.com/package/corsから
Simple Usage (Enable All CORS Requests)
var express = require('express')
var cors = require('cors')
var app = express()
app.use(cors())
app.get('/products/:id', function (req, res, next) {
res.json({msg: 'This is CORS-enabled for all origins!'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
cookie-parser(https://www.npmjs.com/package/cookie-parser)
Example
var express = require('express')
var cookieParser = require('cookie-parser')
var app = express()
app.use(cookieParser())
app.get('/', function (req, res) {
// Cookies that have not been signed
console.log('Cookies: ', req.cookies)
// Cookies that have been signed
console.log('Signed Cookies: ', req.signedCookies)
})
app.listen(8080)
githubにアップされていたコードのコメントがわかりやすいのでそのまま上げます。
require('dotenv/config');
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const { verify } = require('jsonwebtoken');
const { hash, compare } = require('bcryptjs');
const {
createAccessToken,
createRefreshToken,
sendRefreshToken,
sendAccessToken,
} = require('./tokens.js');
const { fakeDB } = require('./fakeDB.js');
const { isAuth } = require('./isAuth.js');
// 1. Register a user
// 2. Login a user
// 3. Logout a user
// 4. Setup a protected route
// 5. Get a new accesstoken with a refresh token
const server = express();
// Use express middleware for easier cookie handling
server.use(cookieParser());
server.use(
cors({
origin: 'http://localhost:3000',
credentials: true,
}),
);
// Needed to be able to read body data
server.use(express.json()); // to support JSON-encoded bodies
server.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies
// 1. Register a user
server.post('/register', async (req, res) => {
const { email, password } = req.body;
try {
// 1. Check if the user exist
const user = fakeDB.find(user => user.email === email);
if (user) throw new Error('User already exist');
// 2. If not user exist already, hash the password
const hashedPassword = await hash(password, 10);
// 3. Insert the user in "database"
fakeDB.push({
id: fakeDB.length,
email,
password: hashedPassword,
});
res.send({ message: 'User Created' });
console.log(fakeDB);
} catch (err) {
res.send({
error: `${err.message}`,
});
}
});
// 2. Login a user
server.post('/login', async (req, res) => {
const { email, password } = req.body;
try {
// 1. Find user in array. If not exist send error
const user = fakeDB.find(user => user.email === email);
if (!user) throw new Error('User does not exist');
// 2. Compare crypted password and see if it checks out. Send error if not
const valid = await compare(password, user.password);
if (!valid) throw new Error('Password not correct');
// 3. Create Refresh- and Accesstoken
const accesstoken = createAccessToken(user.id);
const refreshtoken = createRefreshToken(user.id);
// 4. Store Refreshtoken with user in "db"
// Could also use different version numbers instead.
// Then just increase the version number on the revoke endpoint
user.refreshtoken = refreshtoken;
// 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
sendRefreshToken(res, refreshtoken);
sendAccessToken(res, req, accesstoken);
} catch (err) {
res.send({
error: `${err.message}`,
});
}
});
// 3. Logout a user
server.post('/logout', (_req, res) => {
res.clearCookie('refreshtoken', { path: '/refresh_token' });
// Logic here for also remove refreshtoken from db
return res.send({
message: 'Logged out',
});
});
// 4. Protected route
server.post('/protected', async (req, res) => {
try {
const userId = isAuth(req);
if (userId !== null) {
res.send({
data: 'This is protected data.',
});
}
} catch (err) {
res.send({
error: `${err.message}`,
});
}
});
// 5. Get a new access token with a refresh token
server.post('/refresh_token', (req, res) => {
const token = req.cookies.refreshtoken;
// If we don't have a token in our request
if (!token) return res.send({ accesstoken: '' });
// We have a token, let's verify it!
let payload = null;
try {
payload = verify(token, process.env.REFRESH_TOKEN_SECRET);
} catch (err) {
return res.send({ accesstoken: '' });
}
// token is valid, check if user exist
const user = fakeDB.find(user => user.id === payload.userId);
if (!user) return res.send({ accesstoken: '' });
// user exist, check if refreshtoken exist on user
if (user.refreshtoken !== token)
return res.send({ accesstoken: '' });
// token exist, create new Refresh- and accesstoken
const accesstoken = createAccessToken(user.id);
const refreshtoken = createRefreshToken(user.id);
// update refreshtoken on user in db
// Could have different versions instead!
user.refreshtoken = refreshtoken;
// All good to go, send new refreshtoken and accesstoken
sendRefreshToken(res, refreshtoken);
return res.send({ accesstoken });
});
server.listen(process.env.PORT, () =>
console.log(`Server listening on port ${process.env.PORT}!`),
);
会員登録のメソッドについて
server.post('/register', async (req, res) => {
const { email, password } = req.body;
try {
// 1. Check if the user exist
const user = fakeDB.find(user => user.email === email);
if (user) throw new Error('User already exist');
// 2. If not user exist already, hash the password
const hashedPassword = await hash(password, 10);
// 3. Insert the user in "database"
fakeDB.push({
id: fakeDB.length,
email,
password: hashedPassword,
});
res.send({ message: 'User Created' });
console.log(fakeDB);
} catch (err) {
res.send({
error: `${err.message}`,
});
}
});
DBの中に会員データが見つかったらエラーを返し、見つからなかった場合DBにデータ挿入と板感じですね。
会員ログインのメソッドについて
// 2. Login a user
server.post('/login', async (req, res) => {
const { email, password } = req.body;
try {
// 1. Find user in array. If not exist send error
const user = fakeDB.find(user => user.email === email);
if (!user) throw new Error('User does not exist');
// 2. Compare crypted password and see if it checks out. Send error if not
const valid = await compare(password, user.password);
if (!valid) throw new Error('Password not correct');
// 3. Create Refresh- and Accesstoken
const accesstoken = createAccessToken(user.id);
const refreshtoken = createRefreshToken(user.id);
// 4. Store Refreshtoken with user in "db"
// Could also use different version numbers instead.
// Then just increase the version number on the revoke endpoint
user.refreshtoken = refreshtoken;
// 5. Send token. Refreshtoken as a cookie and accesstoken as a regular response
sendRefreshToken(res, refreshtoken);
sendAccessToken(res, req, accesstoken);
} catch (err) {
res.send({
error: `${err.message}`,
});
}
});
説明しなくても解ると思います。(笑
念の為このコードで使われているAccesstoken、Refreshtokenについては
https://auth0.com/blog/jp-refresh-tokens-what-are-they-and-when-to-use-them/
に書かれている通り
Access Token はリソースに直接アクセスするために必要な情報を保持しています。つまり、クライアントがリソースを管理するサーバーにAccess Tokenをパスするとき、そのサーバーはそのトークンに含まれている情報を使用してクライアントが認可したものかを判断します。Access Tokenには通常、有効期限があり、存続期間は短いです。
Refresh Token は新しいAccess Tokenを取得するために必要な情報を保持しています。つまり、特定リソースにアクセスする際に、Access Tokenが必要な場合には、クライアントはAuthorization Serverが発行する新しいAccess Tokenを取得するためにRefresh Tokenを使用します。一般的な使用方法は、Access Tokenの期限が切れた後に新しいものを取得したり、初めて新しいリソースにアクセスするときなどです。Refresh Tokenにも有効期限がありますが、存続期間はAccess Tokenよりも長くなっています。Refresh Tokenは通常、漏洩しないように厳しいストレージ要件が課せられます。Authorization Serverによってブラックリストに載ることもあります。
src/isAuth.jsについて
const { verify } = require('jsonwebtoken');
const isAuth = req => {
const authorization = req.headers['authorization'];
if (!authorization) throw new Error('You need to login.');
// Based on 'Bearer ksfljrewori384328289398432'
const token = authorization.split(' ')[1];
const { userId } = verify(token, process.env.ACCESS_TOKEN_SECRET);
return userId;
};
module.exports = {
isAuth,
};
jsのsplit文についてはhttps://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/split
var str = 'The quick brown fox jumps over the lazy dog.';
var words = str.split(' ');
console.log(words[3]);
// expected output: "fox"
var chars = str.split('');
console.log(chars[8]);
// expected output: "k"
var strCopy = str.split();
console.log(strCopy);
// expected output: Array ["The quick brown fox jumps over the lazy dog."]
front側については
src/App.js
import React, { useState, useEffect } from 'react';
import { Router, navigate } from '@reach/router';
import Navigation from './components/Navigation';
import Login from './components/Login';
import Register from './components/Register';
import Protected from './components/Protected';
import Content from './components/Content';
export const UserContext = React.createContext([]);
function App() {
const [user, setUser] = useState({});
const [loading, setLoading] = useState(true);
const logOutCallback = async () => {
await fetch('http://localhost:4000/logout', {
method: 'POST',
credentials: 'include', // Needed to include the cookie
});
// Clear user from context
setUser({});
// Navigate back to startpage
navigate('/');
}
// First thing, check if a refreshtoken exist
useEffect(() => {
async function checkRefreshToken() {
const result = await (await fetch('http://localhost:4000/refresh_token', {
method: 'POST',
credentials: 'include', // Needed to include the cookie
headers: {
'Content-Type': 'application/json',
}
})).json();
setUser({
accesstoken: result.accesstoken,
});
setLoading(false);
}
checkRefreshToken();
}, []);
if (loading) return <div>Loading ...</div>
return (
<UserContext.Provider value={[user, setUser]}>
<div className="app">
<Navigation logOutCallback={logOutCallback} />
<Router id="router">
<Login path="login" />
<Register path="register" />
<Protected path="protected" />
<Content path="/" />
</Router>
</div>
</UserContext.Provider>
);
}
export default App;
src/components/Register.jsについては
import React, { useState } from 'react';
import { navigate } from '@reach/router';
const Register = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = async e => {
e.preventDefault();
const result = await (await fetch('http://localhost:4000/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
password: password,
}),
})).json();
if (!result.error) {
console.log(result.message);
navigate('/');
} else {
console.log(result.error);
}
};
const handleChange = e => {
if (e.currentTarget.name === 'email') {
setEmail(e.currentTarget.value);
} else {
setPassword(e.currentTarget.value);
}
};
return (
<div className="login-wrapper">
<form onSubmit={handleSubmit}>
<div>Register</div>
<div className="login-input">
<input
value={email}
onChange={handleChange}
type="text"
name="email"
placeholder="Email"
autoComplete="email"
/>
<input
value={password}
onChange={handleChange}
type="password"
name="password"
autoComplete="current-password"
placeholder="Password"
/>
<button type="submit">Register</button>
</div>
</form>
</div>
);
};
export default Register;
front側についてはそのままありのままなので詳しくは説明できません。
見てくれた方には心苦しいですが、赦してほしいです。
では、また。