Skip to content

Commit b295030

Browse files
committed
refactor(encoding): ♻️ Replaced encoding implementation with undio functions
1 parent ee4c0d3 commit b295030

File tree

6 files changed

+55
-662
lines changed

6 files changed

+55
-662
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
"scule": "^1.3.0",
4444
"sirv": "^2.0.4",
4545
"ufo": "^1.5.4",
46-
"uncrypto": "^0.1.3"
46+
"uncrypto": "^0.1.3",
47+
"undio": "^0.2.0"
4748
},
4849
"devDependencies": {
4950
"@antfu/eslint-config": "^3.7.1",

pnpm-lock.yaml

Lines changed: 34 additions & 577 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/runtime/plugins/provideDefaults.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/* eslint-disable no-console */
22
import { subtle } from 'uncrypto'
3-
import { genBase64FromBytes, generateRandomUrlSafeString } from '../server/utils/security'
3+
import { generateRandomUrlSafeString } from '../server/utils/security'
44
// @ts-expect-error - Missing Nitro type exports in Nuxt
55
import { defineNitroPlugin } from '#imports'
6+
import { arrayBufferToBase64 } from 'undio'
67

78
export default defineNitroPlugin(async () => {
89
if (!process.env.NUXT_OIDC_SESSION_SECRET || process.env.NUXT_OIDC_SESSION_SECRET.length < 48) {
@@ -12,7 +13,7 @@ export default defineNitroPlugin(async () => {
1213
console.info(`[nuxt-oidc-auth]: NUXT_OIDC_SESSION_SECRET=${randomSecret}`)
1314
}
1415
if (!process.env.NUXT_OIDC_TOKEN_KEY) {
15-
const randomKey = genBase64FromBytes(new Uint8Array(await subtle.exportKey('raw', await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']))))
16+
const randomKey = arrayBufferToBase64(await subtle.exportKey('raw', await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])), {})
1617
process.env.NUXT_OIDC_TOKEN_KEY = randomKey
1718
console.warn('[nuxt-oidc-auth]: No refresh token key set, using a random key. Please set NUXT_OIDC_TOKEN_KEY in your environment. Refresh tokens saved in this session will be inaccessible after a server restart.')
1819
console.info(`[nuxt-oidc-auth]: NUXT_OIDC_TOKEN_KEY=${randomKey}`)

src/runtime/server/lib/oidc.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import { subtle } from 'uncrypto'
99
import * as providerPresets from '../../providers'
1010
import { validateConfig } from '../utils/config'
1111
import { configMerger, convertObjectToSnakeCase, convertTokenRequestToType, oidcErrorHandler, useOidcLogger } from '../utils/oidc'
12-
import { encryptToken, genBase64FromString, generatePkceCodeChallenge, generatePkceVerifier, generateRandomUrlSafeString, parseJwtToken, validateToken } from '../utils/security'
12+
import { encryptToken, generatePkceCodeChallenge, generatePkceVerifier, generateRandomUrlSafeString, parseJwtToken, validateToken } from '../utils/security'
1313
import { clearUserSession, getUserSession, getUserSessionId } from '../utils/session'
1414
// @ts-expect-error - Missing Nitro type exports in Nuxt
1515
import { useRuntimeConfig, useStorage } from '#imports'
16+
import { textToBase64 } from 'undio'
1617

1718
async function useAuthSession(event: H3Event) {
1819
const session = await useSession<AuthSession>(event, {
@@ -138,7 +139,7 @@ export function callbackEventHandler({ onSuccess }: OAuthConfig<UserSession>) {
138139

139140
// Validate if authentication information should be send in header or body
140141
if (config.authenticationScheme === 'header') {
141-
const encodedCredentials = genBase64FromString(`${config.clientId}:${config.clientSecret}`)
142+
const encodedCredentials = textToBase64(`${config.clientId}:${config.clientSecret}`, { dataURL: false })
142143
headers.authorization = `Basic ${encodedCredentials}`
143144
}
144145

src/runtime/server/utils/oidc.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { createError } from 'h3'
77
import { ofetch } from 'ofetch'
88
import { snakeCase } from 'scule'
99
import { normalizeURL } from 'ufo'
10-
import { genBase64FromString, parseJwtToken } from './security'
10+
import { textToBase64 } from 'undio'
11+
import { parseJwtToken } from './security'
1112

1213
export function useOidcLogger() {
1314
return createConsola().withDefaults({ tag: 'nuxt-oidc-auth', message: '[nuxt-oidc-auth]:' })
@@ -27,7 +28,7 @@ export async function refreshAccessToken(refreshToken: string, config: OidcProvi
2728

2829
// Validate if authentication information should be send in header or body
2930
if (config.authenticationScheme === 'header') {
30-
const encodedCredentials = genBase64FromString(`${config.clientId}:${config.clientSecret}`)
31+
const encodedCredentials = textToBase64(`${config.clientId}:${config.clientSecret}`, { dataURL: false })
3132
headers.authorization = `Basic ${encodedCredentials}`
3233
}
3334

src/runtime/server/utils/security.ts

Lines changed: 10 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Buffer } from 'node:buffer'
21
import { createRemoteJWKSet, jwtVerify } from 'jose'
32
import { getRandomValues, subtle } from 'uncrypto'
3+
import { arrayBufferToBase64, base64ToText, base64ToUint8Array, uint8ArrayToBase64 } from 'undio'
44
import { useOidcLogger } from './oidc'
55

66
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
@@ -66,7 +66,7 @@ async function encryptMessage(text: string, key: CryptoKey, iv: Uint8Array) {
6666
key,
6767
encoded,
6868
)
69-
return genBase64FromBytes(new Uint8Array(ciphertext))
69+
return arrayBufferToBase64(ciphertext, { urlSafe: false })
7070
}
7171

7272
/**
@@ -76,7 +76,7 @@ async function encryptMessage(text: string, key: CryptoKey, iv: Uint8Array) {
7676
* @returns The decrypted message.
7777
*/
7878
async function decryptMessage(text: string, key: CryptoKey, iv: Uint8Array) {
79-
const decoded = genBytesFromBase64(text)
79+
const decoded = base64ToUint8Array(text)
8080
return await subtle.decrypt({ name: 'AES-GCM', iv }, key, decoded)
8181
}
8282

@@ -106,7 +106,7 @@ export function generatePkceVerifier(length: number = 64) {
106106
*/
107107
export async function generatePkceCodeChallenge(pkceVerifier: string) {
108108
const challengeBuffer = await subtle.digest({ name: 'SHA-256' }, new TextEncoder().encode(pkceVerifier))
109-
return genBase64FromBytes(new Uint8Array(challengeBuffer), true)
109+
return arrayBufferToBase64(challengeBuffer, { urlSafe: true, dataURL: false })
110110
}
111111

112112
/**
@@ -117,7 +117,7 @@ export async function generatePkceCodeChallenge(pkceVerifier: string) {
117117
export function generateRandomUrlSafeString(length: number = 48): string {
118118
const randomBytes = new Uint8Array(length)
119119
getRandomValues(randomBytes)
120-
return genBase64FromString(String.fromCharCode(...randomBytes), { encoding: 'url' }).slice(0, length)
120+
return uint8ArrayToBase64(randomBytes, { urlSafe: true, dataURL: false }).slice(0, length)
121121
}
122122

123123
/**
@@ -127,16 +127,15 @@ export function generateRandomUrlSafeString(length: number = 48): string {
127127
* @returns The base64 encoded encrypted refresh token and the base64 encoded initialization vector.
128128
*/
129129
export async function encryptToken(token: string, key: string): Promise<EncryptedToken> {
130-
// TODO: Replace Buffer
131-
const secretKey = await subtle.importKey('raw', Buffer.from(key, 'base64'), {
130+
const secretKey = await subtle.importKey('raw', base64ToUint8Array(key), {
132131
name: 'AES-GCM',
133132
length: 256,
134133
}, true, ['encrypt', 'decrypt'])
135134
const iv = getRandomValues(new Uint8Array(12))
136135
const encryptedToken = await encryptMessage(token, secretKey, iv)
137136
return {
138137
encryptedToken,
139-
iv: genBase64FromBytes(iv),
138+
iv: uint8ArrayToBase64(iv, { dataURL: false }),
140139
}
141140
}
142141

@@ -148,12 +147,11 @@ export async function encryptToken(token: string, key: string): Promise<Encrypte
148147
*/
149148
export async function decryptToken(input: EncryptedToken, key: string): Promise<string> {
150149
const { encryptedToken, iv } = input
151-
// TODO: Replace Buffer
152-
const secretKey = await subtle.importKey('raw', Buffer.from(key, 'base64'), {
150+
const secretKey = await subtle.importKey('raw', base64ToUint8Array(key), {
153151
name: 'AES-GCM',
154152
length: 256,
155153
}, true, ['encrypt', 'decrypt'])
156-
const decrypted = await decryptMessage(encryptedToken, secretKey, genBytesFromBase64(iv))
154+
const decrypted = await decryptMessage(encryptedToken, secretKey, base64ToUint8Array(iv))
157155
return new TextDecoder().decode(decrypted)
158156
}
159157

@@ -172,12 +170,7 @@ export function parseJwtToken(token: string, skipParsing?: boolean): JwtPayload
172170
const [header, payload, signature, ...rest] = token.split('.')
173171
if (!header || !payload || !signature || rest.length)
174172
throw new Error('Invalid JWT token')
175-
return JSON.parse(genStringFromBase64(payload, { encoding: 'url' }))
176-
/* Full JWT {
177-
header: JSON.parse(genStringFromBase64(header, { encoding: 'url' })),
178-
payload: JSON.parse(genStringFromBase64(payload, { encoding: 'url' })),
179-
signature: genStringFromBase64(signature, { encoding: 'url' }),
180-
} */
173+
return JSON.parse(base64ToText(payload, { urlSafe: true }))
181174
}
182175

183176
export async function validateToken(token: string, options: ValidateAccessTokenOptions): Promise<JwtPayload> {
@@ -188,64 +181,3 @@ export async function validateToken(token: string, options: ValidateAccessTokenO
188181
})
189182
return payload as JwtPayload
190183
}
191-
192-
// Base64 utilities // TODO: Replace with undio
193-
194-
interface CodegenOptions {
195-
encoding?: 'utf8' | 'ascii' | 'url'
196-
}
197-
198-
export function genBytesFromBase64(input: string) {
199-
return Uint8Array.from(
200-
globalThis.atob(input),
201-
c => c.codePointAt(0) as number,
202-
)
203-
}
204-
205-
export function genBase64FromBytes(input: Uint8Array, urlSafe?: boolean) {
206-
if (urlSafe) {
207-
return globalThis
208-
.btoa(String.fromCodePoint(...input))
209-
.replace(/\+/g, '-')
210-
.replace(/\//g, '_')
211-
.replace(/=+$/, '')
212-
}
213-
return globalThis.btoa(String.fromCodePoint(...input))
214-
}
215-
216-
export function genBase64FromString(
217-
input: string,
218-
options: CodegenOptions = {},
219-
) {
220-
if (options.encoding === 'utf8') {
221-
return genBase64FromBytes(new TextEncoder().encode(input))
222-
}
223-
if (options.encoding === 'url') {
224-
return genBase64FromBytes(new TextEncoder().encode(input))
225-
.replace(/\+/g, '-')
226-
.replace(/\//g, '_')
227-
.replace(/=+$/, '')
228-
}
229-
return globalThis.btoa(input)
230-
}
231-
232-
export function genStringFromBase64(
233-
input: string,
234-
options: CodegenOptions = {},
235-
) {
236-
if (options.encoding === 'utf8') {
237-
return new TextDecoder().decode(genBytesFromBase64(input))
238-
}
239-
if (options.encoding === 'url') {
240-
input = input.replace(/-/g, '+').replace(/_/g, '/')
241-
const paddingLength = input.length % 4
242-
if (paddingLength === 2) {
243-
input += '=='
244-
}
245-
else if (paddingLength === 3) {
246-
input += '='
247-
}
248-
return new TextDecoder().decode(genBytesFromBase64(input))
249-
}
250-
return globalThis.atob(input)
251-
}

0 commit comments

Comments
 (0)