Categorygithub.com/restuwahyu13/secure-jwt-token
modulepackage
0.0.0-20241124122858-470a1e0070a2
Repository: https://github.com/restuwahyu13/secure-jwt-token.git
Documentation: pkg.go.dev

# 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, dikarenakan hanya menerapkan JWT Access Token standar implementasi.

Benefit

  • Secure & Strong JWT Token
  • Dynamic Secret Key JWT Token
  • Dynamic Asymmetric Password
  • Strict identity access management
  • Server Side Validation

NodeJS Version

  • JOSE

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<typeof Redis> = new Redis()

  static JweEncrypt(privateKey: jose.KeyLike | crypto.KeyObject, data: string): Promise<jose.FlattenedJWE> {
    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<string> {
    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<jose.KeyLike | Uint8Array> {
    try {
      return jose.importJWK(jwkExport)
    } catch (e: any) {
      throw apiResponse(e)
    }
  }

  static exportJsonWebKey(privateKey: jose.KeyLike | crypto.KeyObject): Promise<jose.JWK> {
    try {
      return jose.exportJWK(privateKey)
    } catch (e: any) {
      throw apiResponse(e)
    }
  }

  static JwtSign(privateKey: jose.KeyLike | crypto.KeyObject, headerKeyId: string, data: Record<string, any>, options: JwtPayload): Promise<string> {
    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<jose.JWTVerifyResult<jose.JWTPayload>> {
    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

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<typeof Redis> = 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<string, string> = 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<ISignatureMetadata> {
    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 = rsaPrivateKey

      return this.sigMetadata
    } catch (e: any) {
      throw apiResponse(e)
    }
  }

  async sign(prefix: string, body: any): Promise<string> {
    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.jwtExpired

        const 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<jose.JWTVerifyResult<jose.JWTPayload>> {
    try {
      return new Jose().JwtVerify(prefix, token)
    } catch (e: any) {
      throw apiResponse(e)
    }
  }
}
  • Middleware Auth

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<OutgoingMessage> {
    try {
      const jwt: InstanceType<typeof JsonWebToken> = new JsonWebToken()
      const headers: Record<string, any> = req.headers

      if (!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<JWTPayload> = 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)
    }
  }
}

# Packages

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Structs

No description provided by the author
No description provided by the author