Skip to content

Instantly share code, notes, and snippets.

@FireNeslo
Last active March 24, 2024 04:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FireNeslo/81e0e5a6be88c628c4247a9b82ca1c59 to your computer and use it in GitHub Desktop.
Save FireNeslo/81e0e5a6be88c628c4247a9b82ca1c59 to your computer and use it in GitHub Desktop.
JWT tokens using web crypto
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const ALGORITHMS = {
HS: { name: 'HMAC' },
ES: { name: 'ECDSA', namedCurve: 'P-256' },
RS: { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]) },
PS: { name: 'RSA-PSS', saltLength: 128, modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]) },
}
function getAlgorithm(name) {
const [, alg, bits ] = /^([A-Z]{2})([\d]{3})$/.exec(name.toUpperCase())
return {
...ALGORITHMS[alg],
hash: {
name: `SHA-${bits}`
}
}
}
export class Base64 {
static encode(buffer) {
if(typeof buffer === 'string') {
return Base64.encode(encoder.encode(buffer))
}
return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
static decode(string) {
return Uint8Array.from(atob(string.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), c => c.charCodeAt(0))
}
static stringify(buffer) {
if(typeof buffer === 'string') {
return Base64.stringify(Base64.decode(buffer))
}
return decoder.decode(buffer)
}
}
export class JWT {
static async sign(payload, options = { secret: '' }) {
const alg = options.algorithm || (options.secret ? 'HS256' : 'none')
const [ meta, claims ] = [ { typ: 'JWT', alg }, payload ].map(JSON.stringify).map(Base64.encode)
const algorithm = getAlgorithm(alg)
if(!algorithm) return [ meta, claims ].join('.') + '.'
const key = await JWT.key(options.secret, algorithm, 'sign')
const message = meta + '.' + claims
const signature = await crypto.subtle.sign(algorithm, key, encoder.encode(message))
return message + '.' + Base64.encode(signature)
}
static async verify(token, options = { secret: '' }) {
const [ header, payload, signature ] = token.split('.').map(Base64.decode)
const [ meta, claims ] = [ header, payload ].map(Base64.stringify).map(JSON.parse)
const algorithm = getAlgorithm(meta.alg)
const key = await JWT.key(options.secret, algorithm, 'verify')
const message = token.slice(0, token.lastIndexOf('.'))
const verified = await crypto.subtle.verify(algorithm, key, signature, encoder.encode(message))
if(verified) {
return claims
}
return null
}
static async key(secret, algorithm, mode = 'sign') {
if(typeof secret !== 'string') {
return secret
}
return crypto.subtle.importKey('raw', encoder.encode(secret), algorithm, false, [mode])
}
static async keypair({ algorithm = 'RS256', extractable = false } = {}) {
return crypto.subtle.generateKey(getAlgorithm(algorithm), extractable, ["sign", "verify"])
}
}
export default JWT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment