Skip to content

Commit

Permalink
Merge pull request #109 from autonomys/feat/custom-jwt
Browse files Browse the repository at this point in the history
Implement custom JWT system
  • Loading branch information
clostao authored Nov 27, 2024
2 parents 80b428d + 2e0fb0d commit 059887e
Show file tree
Hide file tree
Showing 28 changed files with 1,206 additions and 375 deletions.
1 change: 1 addition & 0 deletions backend/.env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
JWT_SECRET=secret
Binary file modified backend/.yarn/install-state.gz
Binary file not shown.
126 changes: 126 additions & 0 deletions backend/__tests__/e2e/users/jwt.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {
CustomAccessTokenPayload,
CustomRefreshTokenPayload,
} from '../../../src/models/users/jwt'
import { CustomJWTAuth } from '../../../src/services/authManager/providers/custom'
import jwt from 'jsonwebtoken'
import { AuthManager } from '../../../src/services/authManager'
import { closeDatabase, getDatabase } from '../../../src/drivers/pg'
import { dbMigration } from '../../utils/dbMigrate'

describe('JWT', () => {
const provider = 'custom'
const userId = '123'

let refreshTokenId: string
let accessTokenString: string
let refreshTokenString: string

beforeAll(async () => {
await getDatabase()
await dbMigration.up()
})

afterAll(async () => {
await closeDatabase()
await dbMigration.down()
})

it('should generate a JWT token', async () => {
const token = await CustomJWTAuth.createSessionTokens({
id: userId,
provider,
})

const refreshTokenDecoded = jwt.decode(token.refreshToken)
if (typeof refreshTokenDecoded === 'string' || !refreshTokenDecoded) {
throw new Error('Invalid refresh token')
}
const refreshTokenPayload = refreshTokenDecoded as CustomRefreshTokenPayload

expect(refreshTokenPayload.oauthUserId).toBe(userId)
expect(refreshTokenPayload.isRefreshToken).toBe(true)
expect(refreshTokenPayload.id).toBeDefined()

refreshTokenId = refreshTokenPayload.id

const decoded = jwt.decode(token.accessToken)
expect(decoded).toBeDefined()
if (typeof decoded === 'string' || !decoded) {
throw new Error('Invalid access token')
}
const accessTokenPayload = decoded as CustomAccessTokenPayload

expect(accessTokenPayload.oauthProvider).toBe(provider)
expect(accessTokenPayload.oauthUserId).toBe(userId)
expect(accessTokenPayload.isRefreshToken).toBe(false)
expect(accessTokenPayload.refreshTokenId).toBe(refreshTokenId)

accessTokenString = token.accessToken
refreshTokenString = token.refreshToken
})

it('should be able to authenticate with the access token', async () => {
const user = await AuthManager.getUserFromAccessToken(
'custom-jwt',
accessTokenString,
)

expect(user).toBeDefined()
expect(user?.id).toBe(userId)
expect(user?.provider).toBe(provider)
})

it('should be able to refresh with the refresh token', async () => {
const newAccessToken =
await CustomJWTAuth.refreshAccessToken(refreshTokenString)

if (!newAccessToken) {
expect(newAccessToken).not.toBeNull()
return
}

const decoded = jwt.decode(newAccessToken)
expect(decoded).toBeDefined()
if (typeof decoded === 'string' || !decoded) {
throw new Error('Invalid access token')
}

const accessTokenPayload = decoded as CustomAccessTokenPayload

expect(accessTokenPayload.refreshTokenId).toBe(refreshTokenId)
expect(accessTokenPayload.isRefreshToken).toBe(false)
expect(accessTokenPayload.oauthUserId).toBe(userId)
expect(accessTokenPayload.oauthProvider).toBe(provider)

accessTokenString = newAccessToken
})

it('should not be able to refresh with the access token', async () => {
expect(CustomJWTAuth.refreshAccessToken(accessTokenString)).rejects.toThrow(
'Invalid refresh token',
)
})

it('should be able to invalidate the refresh token', async () => {
expect(() =>
CustomJWTAuth.invalidateRefreshToken(refreshTokenString),
).not.toThrow()
})

it('should not be able to generate an access token after invalidating the refresh token', async () => {
await CustomJWTAuth.invalidateRefreshToken(refreshTokenString)

await expect(
CustomJWTAuth.refreshAccessToken(refreshTokenString),
).rejects.toThrow('Invalid refresh token')
})

it('should not be able to authenticate with the access token after invalidating the refresh token', async () => {
await CustomJWTAuth.invalidateRefreshToken(refreshTokenString)

await expect(
AuthManager.getUserFromAccessToken('custom-jwt', accessTokenString),
).rejects.toThrow('Invalid access token')
})
})
2 changes: 2 additions & 0 deletions backend/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PostgreSqlContainer } from '@testcontainers/postgresql'
import { config } from 'dotenv'

export default async () => {
config({ path: '.env.test' })
const container = new PostgreSqlContainer().withExposedPorts(54320)
const service = await container.start()
process.env.DATABASE_URL = service.getConnectionUri()
Expand Down
57 changes: 57 additions & 0 deletions backend/migrations/20241125160059-jwt-token-registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict'

var dbm
var type
var seed
var fs = require('fs')
var path = require('path')
var Promise

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function (options, seedLink) {
dbm = options.dbmigrate
type = dbm.dataType
seed = seedLink
Promise = options.Promise
}

exports.up = function (db) {
var filePath = path.join(
__dirname,
'sqls',
'20241125160059-jwt-token-registry-up.sql',
)
return new Promise(function (resolve, reject) {
fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) {
if (err) return reject(err)

resolve(data)
})
}).then(function (data) {
return db.runSql(data)
})
}

exports.down = function (db) {
var filePath = path.join(
__dirname,
'sqls',
'20241125160059-jwt-token-registry-down.sql',
)
return new Promise(function (resolve, reject) {
fs.readFile(filePath, { encoding: 'utf-8' }, function (err, data) {
if (err) return reject(err)

resolve(data)
})
}).then(function (data) {
return db.runSql(data)
})
}

exports._meta = {
version: 1,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS jwt_token_registry;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE jwt_token_registry (
id TEXT PRIMARY KEY,
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
8 changes: 5 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
"@polkadot/api": "^12.3.1",
"@polkadot/types": "^13.0.1",
"@polkadot/util-crypto": "^13.0.2",
"@types/express": "^4.17.21",
"@types/lru-cache": "^7.10.10",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"db-migrate": "^0.11.14",
"db-migrate-pg": "^1.5.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"multiformats": "^13.2.2",
"pg": "^8.13.0",
Expand All @@ -40,8 +39,11 @@
"@swc/jest": "^0.2.37",
"@testcontainers/postgresql": "^10.14.0",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.12",
"@types/multer": "^1",
"@types/jsonwebtoken": "^9",
"@types/lru-cache": "^7.10.10",
"@types/multer": "^1.4.12",
"@types/node": "^22.3.0",
"@types/pg": "^8.11.10",
"@types/pg-format": "^1",
Expand Down
15 changes: 10 additions & 5 deletions backend/src/controllers/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ objectController.get('/search', async (req, res) => {
}

if (typeof cid !== 'string') {
return res.status(400).json({
res.status(400).json({
error: 'Missing or invalid cid value',
})
return
}

const limit = req.query.limit
Expand Down Expand Up @@ -117,9 +118,10 @@ objectController.get('/:cid/metadata', async (req, res) => {
const { cid } = req.params
const metadata = await ObjectUseCases.getMetadata(cid)
if (!metadata) {
return res.status(404).json({
res.status(404).json({
error: 'Metadata not found',
})
return
}

res.json(metadata)
Expand All @@ -144,9 +146,10 @@ objectController.post('/:cid/share', async (req, res) => {
const { cid } = req.params

if (!publicId) {
return res.status(400).json({
res.status(400).json({
error: 'Missing `publicId` in request body',
})
return
}

const user = await handleAuth(req, res)
Expand Down Expand Up @@ -177,9 +180,10 @@ objectController.get('/:cid/download', async (req, res) => {

const metadata = await ObjectUseCases.getMetadata(cid)
if (!metadata) {
return res.status(404).json({
res.status(404).json({
error: 'Metadata not found',
})
return
}

console.log(`Attempting to retrieve data for metadataCid: ${cid}`)
Expand Down Expand Up @@ -263,9 +267,10 @@ objectController.get('/:cid', async (req, res) => {
const objectInformation = await ObjectUseCases.getObjectInformation(cid)

if (!objectInformation) {
return res.status(404).json({
res.status(404).json({
error: 'Object not found',
})
return
}

res.json(objectInformation)
Expand Down
Loading

0 comments on commit 059887e

Please sign in to comment.