Authorizing user on login, setting refresh token and access token on login, creating the session and storing JWT with session in DB, decoding JWT from DB while updating refresh and session tokens.

This commit is contained in:
Bradley Shellnut 2021-03-08 16:10:43 -08:00
parent ea41b550ad
commit 594cd52866
10 changed files with 431 additions and 8 deletions

12
AccessVsRefresh.txt Normal file
View file

@ -0,0 +1,12 @@
Access Token
* JWT
* Contains all of the info someone needs to be logged
* Says this user has Access
* Only available in current session
Refresh Token
* JWT
* Only contains session id
* If valid, then it is used to generate new access Token
* Used to refresh the access token

222
package-lock.json generated
View file

@ -11,7 +11,9 @@
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"fastify": "^3.12.0",
"fastify-cookie": "^5.1.0",
"fastify-static": "^4.0.1",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.6.4"
}
},
@ -134,6 +136,11 @@
"node": ">=0.6.19"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -158,6 +165,14 @@
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz",
"integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@ -216,6 +231,14 @@
"node": ">=8"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -309,6 +332,16 @@
"node": ">=10.16.0"
}
},
"node_modules/fastify-cookie": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-cookie/-/fastify-cookie-5.1.0.tgz",
"integrity": "sha512-AN5C/p7YVSgnW1D9fcUL10yRIN+9lcOtyps3h4/5ZsxwrHVgdNH5T77CbnIrzfAx6qz7K/8NYQCTE8cxZIJcJg==",
"dependencies": {
"cookie": "^0.4.0",
"cookie-signature": "^1.1.0",
"fastify-plugin": "^3.0.0"
}
},
"node_modules/fastify-error": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.0.tgz",
@ -441,6 +474,54 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"node_modules/jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=4",
"npm": ">=1.4.28"
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/light-my-request": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.4.1.tgz",
@ -453,6 +534,41 @@
"set-cookie-parser": "^2.4.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -996,6 +1112,11 @@
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
"integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg=="
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1014,6 +1135,11 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
},
"cookie-signature": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz",
"integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A=="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@ -1052,6 +1178,14 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1130,6 +1264,16 @@
"tiny-lru": "^7.0.0"
}
},
"fastify-cookie": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-cookie/-/fastify-cookie-5.1.0.tgz",
"integrity": "sha512-AN5C/p7YVSgnW1D9fcUL10yRIN+9lcOtyps3h4/5ZsxwrHVgdNH5T77CbnIrzfAx6qz7K/8NYQCTE8cxZIJcJg==",
"requires": {
"cookie": "^0.4.0",
"cookie-signature": "^1.1.0",
"fastify-plugin": "^3.0.0"
}
},
"fastify-error": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.0.tgz",
@ -1244,6 +1388,49 @@
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"requires": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"light-my-request": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.4.1.tgz",
@ -1256,6 +1443,41 @@
"set-cookie-parser": "^2.4.1"
}
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",

View file

@ -13,7 +13,9 @@
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"fastify": "^3.12.0",
"fastify-cookie": "^5.1.0",
"fastify-static": "^4.0.1",
"jsonwebtoken": "^8.5.1",
"mongodb": "^3.6.4"
}
}

View file

@ -12,7 +12,6 @@ export async function authorizeUser(email, password) {
const savedPassword = userData.password
// Compare password with one in database
const isAuthorized = await compare(password, savedPassword)
console.log("isAuthorized", isAuthorized)
// Return boolean of if password is correct
return isAuthorized
return { isAuthorized, userId: userData._id }
}

27
src/accounts/logUserIn.js Normal file
View file

@ -0,0 +1,27 @@
import { createSession } from './session.js'
import { createTokens } from './tokens.js'
export async function logUserIn(userId, request, reply) {
const connectionInformation = {
ip: request.ip,
userAgent: request.headers['user-agent'],
}
// Create Session
const sessionToken = await createSession(userId, connectionInformation)
// Create JWT
const { accessToken, refreshToken } = await createTokens(sessionToken, userId)
// Set Cookie
const now = new Date()
// Get date, 30 days in the future
const refreshExpires = now.setDate(now.getDate() + 30)
reply.setCookie('refreshToken', refreshToken, {
path: "/",
domain: "localhost",
httpOnly: true,
expires: refreshExpires,
}).setCookie('accessToken', accessToken, {
path: "/",
domain: "localhost",
httpOnly: true,
})
}

25
src/accounts/session.js Normal file
View file

@ -0,0 +1,25 @@
import { randomBytes } from 'crypto'
export async function createSession(userId, connection) {
try {
// Generate a session token
const sessionToken = randomBytes(43).toString("hex")
// Retrieve connection information
const { ip, userAgent } = connection
// database insert for ession
const { session } = await import('../session/session.js')
await session.insertOne({
sessionToken,
userId,
valid: true,
userAgent,
ip,
updatedAt: new Date(),
createdAt: new Date(),
})
// Return session token
return sessionToken
} catch (e) {
throw new Error('Session Creation Failed')
}
}

29
src/accounts/tokens.js Normal file
View file

@ -0,0 +1,29 @@
import jwt from 'jsonwebtoken'
const JWTSignature = process.env.JWT_SIGNATURE
export async function createTokens(sessionToken, userId) {
try {
// Create Refresh Token
// Session Id
const refreshToken = jwt.sign(
{
sessionToken
},
JWTSignature
)
// Create Access Token
// Session Id, User Id
const accessToken = jwt.sign(
{
sessionToken,
userId,
},
JWTSignature
)
// Return Refresh Token & Access Token
return { accessToken, refreshToken }
} catch (e) {
console.error('e', e)
}
}

73
src/accounts/user.js Normal file
View file

@ -0,0 +1,73 @@
import mongo from 'mongodb'
import jwt from 'jsonwebtoken'
import { createTokens } from './tokens.js'
const { ObjectId } = mongo
const JWTSignature = process.env.JWT_SIGNATURE
export async function getUserFromCookies(request, reply) {
try {
const { user } = await import("../user/user.js")
const { session } = await import("../session/session.js")
// Check to make sure access token exists
if (request?.cookies?.accessToken) {
// If access token
const { accessToken } = request.cookies
// Decode access token
const decodedAccessToken = jwt.verify(accessToken, JWTSignature)
// Return user from record
return user.findOne({
_id: ObjectId(decodedAccessToken?.userId),
})
}
if (request?.cookies?.refreshToken) {
const { refreshToken } = request.cookies
// Decode refresh token
const { sessionToken } = jwt.verify(refreshToken, JWTSignature)
// Look up session
const currentSession = await session.findOne({ sessionToken })
// Confirm session is valid
if (currentSession.valid) {
// Look up current user
const currentUser = await user.findOne({
_id: ObjectId(currentSession.userId)
})
console.log('currentUser', currentUser);
// Refresh tokens
await refreshTokens(sessionToken, currentUser._id, reply)
// Retrun current user
return currentUser
} else {
// bad session
}
}
} catch (e) {
console.error(e)
}
}
export async function refreshTokens(sessionToken, userId, reply) {
try {
// Create JWT
const { accessToken, refreshToken } = await createTokens(sessionToken, userId)
// Set Cookie
const now = new Date()
// Get date, 30 days in the future
const refreshExpires = now.setDate(now.getDate() + 30)
reply
.setCookie('refreshToken', refreshToken, {
path: "/",
domain: "localhost",
httpOnly: true,
expires: refreshExpires,
}).setCookie('accessToken', accessToken, {
path: "/",
domain: "localhost",
httpOnly: true,
})
} catch (e) {
console.error(e)
}
}

View file

@ -1,11 +1,14 @@
import './env.js'
import { fastify } from 'fastify'
import fastifyStatic from 'fastify-static'
import fastifyCookie from 'fastify-cookie';
import path from 'path'
import { fileURLToPath } from 'url'
import { connectDb } from './db.js'
import { registerUser } from './accounts/register.js'
import { authorizeUser } from './accounts/authorize.js'
import { logUserIn } from './accounts/logUserIn.js'
import { getUserFromCookies } from './accounts/user.js'
// ESM specific "features"
const __filename = fileURLToPath(import.meta.url)
@ -15,6 +18,10 @@ const app = fastify()
async function startApp() {
try {
app.register(fastifyCookie, {
secret: process.env.COOKIE_SIGNATURE,
})
app.register(fastifyStatic, {
root: path.join(__dirname, "public"),
})
@ -34,19 +41,42 @@ async function startApp() {
app.post('/api/authorize', {}, async (request, reply) => {
try {
console.log(request.body.email, request.body.password)
const userId = await authorizeUser(
const { isAuthorized, userId } = await authorizeUser(
request.body.email,
request.body.password
)
if (isAuthorized) {
await logUserIn(userId, request, reply)
reply.send({
data: "User Logged In",
})
}
reply.send({
data: "Auth Failed",
})
} catch (e) {
console.error('e', e);
}
})
// app.get("/", {}, (request, reply) => {
// reply.send({
// data: "hello world",
// })
// })
app.get("/test", {}, async (request, reply) => {
try {
// Verify user login
const user = await getUserFromCookies(request, reply)
// Return user email, if it exists, otherwise return unauthorized
if (user?._id) {
reply.send({
data: user,
})
} else {
reply.send({
data: "User Loopup Failed"
})
}
} catch (e) {
throw new Error(e)
}
})
await app.listen(3000);
console.log('🚀 Server Listening at port: 3000');

4
src/session/session.js Normal file
View file

@ -0,0 +1,4 @@
import { client } from '../db.js'
export const session = client.db("test").collection("session")