https://github.com/restuwahyu13/secure-jwt-token
Example implementation secure jwt access token
https://github.com/restuwahyu13/secure-jwt-token
jwt-token rsa-cryptography rsa-key-encryption
Last synced: 5 days ago
JSON representation
Example implementation secure jwt access token
- Host: GitHub
- URL: https://github.com/restuwahyu13/secure-jwt-token
- Owner: restuwahyu13
- Created: 2024-11-17T18:06:14.000Z (5 months ago)
- Default Branch: master
- Last Pushed: 2024-11-24T12:29:05.000Z (5 months ago)
- Last Synced: 2025-03-29T11:05:16.394Z (26 days ago)
- Topics: jwt-token, rsa-cryptography, rsa-key-encryption
- Language: Go
- Homepage:
- Size: 38.1 KB
- Stars: 5
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Secure Jwt Access Token
Berikut adalah bagaimana cara membuat, **Secure** dan **Strong** `JWT Access Token` dengan menggunakan `Asymmetric Cryptography` dengan `Jose Library`, cara ini saya buat kurang lebih 1 tahun yang lalu ketika saya masih berkerja di `pazemo`, yang dimana tujuan dari tutorial yang saya buat ini, adalah untuk meminimalisir terjadi nya authentication bypass seperti video attacker berikut ini [JWT Authentication Bypass](https://www.youtube.com/watch?v=ov9yT4WAuzI), dikarenakan hanya menerapkan `JWT Access Token` standar implementasi.
## Benefit
- [x] Secure & Strong JWT Token
- [x] Dynamic Secret Key JWT Token
- [x] Dynamic Asymmetric Password
- [x] Strict identity access management
- [x] Server Side Validation## NodeJS Version
- ### JOSE
```ts
import crypto from 'node:crypto'
import * as jose from 'jose'
import { JwtPayload } from 'jsonwebtoken'import { ISignatureMetadata } from '~/libs/lib.jwt'
import { Redis } from '~/libs/lib.redis'
import { apiResponse } from '~/helpers/helper.apiResponse'export class Jose {
private redis: InstanceType = new Redis()static JweEncrypt(privateKey: jose.KeyLike | crypto.KeyObject, data: string): Promise {
try {
const text: Uint8Array = new TextEncoder().encode(data)
const jwe: jose.FlattenedEncrypt = new jose.FlattenedEncrypt(text).setProtectedHeader({
alg: 'RSA-OAEP',
enc: 'A256CBC-HS512',
typ: 'JWT',
cty: 'JWT',
})return jwe.encrypt(privateKey)
} catch (e: any) {
throw apiResponse(e)
}
}static async JweDecerypt(privateKey: jose.KeyLike | crypto.KeyObject, jweEncryption: jose.FlattenedJWE): Promise {
try {
const jwe: jose.FlattenedDecryptResult = await jose.flattenedDecrypt(jweEncryption, privateKey)
const text: string = new TextDecoder().decode(jwe.plaintext)return text
} catch (e: any) {
throw apiResponse(e)
}
}static importJsonWebKey(jwkExport: jose.JWK): Promise {
try {
return jose.importJWK(jwkExport)
} catch (e: any) {
throw apiResponse(e)
}
}static exportJsonWebKey(privateKey: jose.KeyLike | crypto.KeyObject): Promise {
try {
return jose.exportJWK(privateKey)
} catch (e: any) {
throw apiResponse(e)
}
}static JwtSign(privateKey: jose.KeyLike | crypto.KeyObject, headerKeyId: string, data: Record, options: JwtPayload): Promise {
try {
return new jose.SignJWT(data)
.setProtectedHeader({ alg: 'RS512', typ: 'JWT', cty: 'JWT', kid: headerKeyId, b64: true })
.setAudience(options.aud)
.setIssuer(options.iss)
.setSubject(options.sub)
.setIssuedAt(options.iat)
.setExpirationTime(options.exp)
.setJti(options.jti)
.sign(privateKey)
} catch (e: any) {
throw apiResponse(e)
}
}async JwtVerify(prefix: string, token: string): Promise> {
try {
const signatureKey: string = `${prefix}:credential`
const signatureMetadataField: string = 'signature_metadata'const signature: ISignatureMetadata = await this.redis.hget(signatureKey, signatureMetadataField)
if (!signature) {
throw new Error('Invalid signature 1')
}const rsaPrivateKey: crypto.KeyObject = crypto.createPrivateKey({ key: signature.privKeyRaw, passphrase: signature.cipherKey })
if (!rsaPrivateKey) {
throw new Error('Invalid signature 2')
}const jwsVerify: jose.CompactVerifyResult = await jose.compactVerify(token, rsaPrivateKey)
if (jwsVerify.protectedHeader.kid !== signature.jweKey.ciphertext) {
throw new Error('Invalid signature 3')
}const aud: string = signature.sigKey.substring(10, 25)
const iss: string = signature.sigKey.substring(20, 35)
const sub: string = signature.sigKey.substring(40, 55)return jose.jwtVerify(token, rsaPrivateKey, {
audience: aud,
issuer: iss,
subject: sub,
algorithms: [jwsVerify.protectedHeader.alg],
typ: jwsVerify.protectedHeader.typ,
})
} catch (e: any) {
throw apiResponse(e)
}
}
}
```- ### JWT
```ts
import crypto from 'node:crypto'
import * as jose from 'jose'
import moment from 'moment-timezone'import { Redis } from '~/libs/lib.redis'
import { Encryption } from '~/helpers/helper.encryption'
import { Jose } from '~/libs/lib.jose'
import { Environment } from '~/configs/config.env'
import { apiResponse } from '~/helpers/helper.apiResponse'export interface ISecretMetadata {
privKeyRaw: string
pubKeyRaw: string
cipherKey: string
}export interface ISignatureMetadata {
privKey?: crypto.KeyObject
privKeyRaw: string
sigKey: string
cipherKey: string
jweKey: jose.FlattenedJWE
}export class JsonWebToken {
private redis: InstanceType = new Redis()
private keyLength: number = 4096
private jwtExpired: number = Environment.JWT_EXPIRED
private certMetadata: ISecretMetadata = {
privKeyRaw: '',
pubKeyRaw: '',
cipherKey: '',
}
private sigMetadata: ISignatureMetadata = {
privKeyRaw: '',
privKey: {} as any,
sigKey: '',
cipherKey: '',
jweKey: {} as any,
}private createSecret(prefix: string, body: string): ISecretMetadata {
try {
const randomString: string = crypto.randomBytes(16).toString('hex')const cipherTextRandom: string = `${prefix}:${body}:${randomString}:${this.jwtExpired}`
const cipherTextData: string = Buffer.from(cipherTextRandom).toString('hex')const cipherSecretKey: string = crypto.createHash('SHA512').update(cipherTextData).digest().toString('hex')
const cipherText: string = crypto.createHash('SHA512').update(randomString).digest().toString('hex')
const cipherKey: string = Encryption.AES256Encrypt(cipherSecretKey, cipherText).toString('hex')const genCert: crypto.KeyPairSyncResult = crypto.generateKeyPairSync('rsa', {
modulusLength: this.keyLength,
publicKeyEncoding: {
type: 'pkcs1',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc',
passphrase: cipherKey,
},
})this.certMetadata = {
privKeyRaw: genCert.privateKey,
pubKeyRaw: genCert.publicKey,
cipherKey: cipherKey,
}return this.certMetadata
} catch (e: any) {
throw apiResponse(e)
}
}private async createSignature(prefix: string, body: any): Promise {
try {
const signatureKey: string = `${prefix}:credential`
const signatureField: string = 'signature_metadata'body = Buffer.from(JSON.stringify(body))
const secretKey: ISecretMetadata = this.createSecret(prefix, body)const rsaPrivateKey: crypto.KeyObject = crypto.createPrivateKey({
key: Buffer.from(secretKey.privKeyRaw),
type: 'pkcs8',
format: 'pem',
passphrase: secretKey.cipherKey,
})const rsaPublicKey: crypto.KeyObject = crypto.createPublicKey({
key: Buffer.from(secretKey.pubKeyRaw),
type: 'pkcs1',
format: 'pem',
})const cipherHash512: Buffer = crypto.sign('RSA-SHA512', body, rsaPrivateKey)
const signatureOutput: string = cipherHash512.toString('hex')const verifiedSignature = crypto.verify('RSA-SHA512', body, rsaPublicKey, cipherHash512)
if (!verifiedSignature) throw new Error('Invalid signature')const jweKey: jose.FlattenedJWE = await Jose.JweEncrypt(rsaPrivateKey, signatureOutput)
if (!jweKey) throw new Error('Invalid encrypt')this.sigMetadata = {
privKeyRaw: secretKey.privKeyRaw,
sigKey: signatureOutput,
cipherKey: secretKey.cipherKey,
jweKey: jweKey,
}await this.redis.hsetEx(signatureKey, signatureField, this.jwtExpired, this.sigMetadata)
this.sigMetadata.privKey = rsaPrivateKeyreturn this.sigMetadata
} catch (e: any) {
throw apiResponse(e)
}
}async sign(prefix: string, body: any): Promise {
try {
const tokenKey: string = `${prefix}:token`
const tokenExist: number = await this.redis.exists(tokenKey)if (tokenExist < 1) {
const signature: ISignatureMetadata = await this.createSignature(prefix, body)
const timestamp: string = moment().format('YYYY/MM/DD HH:mm:ss')const aud: string = signature.sigKey.substring(10, 25)
const iss: string = signature.sigKey.substring(20, 35)
const sub: string = signature.sigKey.substring(40, 55)const secretKey: string = `${aud}:${iss}:${sub}:${this.jwtExpired}`
const secretData: string = Buffer.from(secretKey).toString('hex')const jti: string = Encryption.AES256Encrypt(secretData, prefix).toString('hex')
const iat: number = Math.floor(Date.now() / 1000) + 60 * 60
const exp: number = iat + this.jwtExpiredconst tokenData: string = await Jose.JwtSign(
signature.privKey,
signature.jweKey.ciphertext,
{ timestamp: timestamp },
{
jti: jti,
aud: aud,
iss: iss,
sub: sub,
iat: iat,
exp: exp,
},
)this.redis.setEx(tokenKey, this.jwtExpired, tokenData)
return tokenData
} else {
return this.redis.get(tokenKey)
}
} catch (e: any) {
throw apiResponse(e)
}
}verify(prefix: string, token: string): Promise> {
try {
return new Jose().JwtVerify(prefix, token)
} catch (e: any) {
throw apiResponse(e)
}
}
}
```
- ### Middleware Auth```ts
import { NextFunction, Request, Response } from 'express'
import { OutgoingMessage } from 'node:http'
import { StatusCodes as status } from 'http-status-codes'
import jsonwebtoken, { JwtPayload } from 'jsonwebtoken'
import validator from 'validator'
import { JWTPayload, JWTVerifyResult } from 'jose'import { apiResponse } from '~/helpers/helper.apiResponse'
import { Container, Injectable } from '~/helpers/helper.di'
import { Encryption } from '~/helpers/helper.encryption'
import { JsonWebToken } from '~/libs/lib.jwt'@Injectable()
export class AuthMiddleware {
async use(req: Request, res: Response, next: NextFunction): Promise {
try {
const jwt: InstanceType = new JsonWebToken()
const headers: Record = req.headersif (!headers.hasOwnProperty('authorization')) {
throw apiResponse({ stat_code: status.UNAUTHORIZED, error: 'Authorization required' })
} else if (!Array.isArray(headers.authorization.match('Bearer'))) {
throw apiResponse({ stat_code: status.UNAUTHORIZED, error: 'Unauthorized invalid token' })
}let authToken: string = headers.authorization.split('Bearer ')[1]
if (!validator.isJWT(authToken)) {
throw apiResponse({ stat_code: status.UNAUTHORIZED, error: 'Unauthorized invalid token' })
}const jwtDecode: JwtPayload = jsonwebtoken.decode(authToken) as any
if (!jwtDecode) {
throw apiResponse({ stat_code: status.UNAUTHORIZED, error: 'Unauthorized invalid token' })
}const secretKey: string = Buffer.from(`${jwtDecode.aud}:${jwtDecode.iss}:${jwtDecode.sub}:${process.env.JWT_EXPIRED}`).toString('hex')
const secretData: Buffer = Buffer.from(jwtDecode.jti, 'hex')
const jti: string = Encryption.AES256Decrypt(secretKey, secretData).toString()const verifyRes: JWTVerifyResult = await jwt.verify(jti, authToken)
if (!verifyRes) {
throw apiResponse({ stat_code: status.UNAUTHORIZED, error: 'Unauthorized invalid token' })
}const userId: string = jti
Container.register('User', { useValue: userId })next()
} catch (e: any) {
return apiResponse({ stat_code: status.UNAUTHORIZED, error: 'Unauthorized invalid token' }, res)
}
}
}
```