mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-28 02:53:15 -04:00
feat(auth): prepend 'Bearer' to auth token generated by tokenHelper (#11097)
* fix(auth-header): decode _password from base64 for default registry auth * fix(auth): prepend 'Bearer ' to auth token generated by tokenHelper * test: skip flaky parallel dlx test on Node 25 * fix(auth): improve tokenHelper Bearer prefix with validation and generic scheme detection - Throw an error when the token helper returns an empty token instead of producing an invalid "Bearer " header - Use a generic auth scheme regex instead of hardcoding only Bearer/Basic, so other schemes (Token, Negotiate, etc.) are preserved as-is - Add tests for raw token prefixing, existing scheme preservation, and empty token error --------- Co-authored-by: Zoltan Kochan <z@kochan.io>
This commit is contained in:
6
.changeset/fix-token-helper-bearer-prefix.md
Normal file
6
.changeset/fix-token-helper-bearer-prefix.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/network.auth-header": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Prepended `Bearer` to the authorization token generated by `tokenHelper` executable if it is missing, properly aligning pnpm's handling of token helpers with npm.
|
||||
@@ -72,5 +72,14 @@ export function loadToken (helperPath: string, settingName: string): string {
|
||||
if (spawnResult.status !== 0) {
|
||||
throw new PnpmError('TOKEN_HELPER_ERROR_STATUS', `Error running "${helperPath}" as a token helper, configured as ${settingName}. Exit code ${spawnResult.status?.toString() ?? ''}`)
|
||||
}
|
||||
return spawnResult.stdout.toString('utf8').trimEnd()
|
||||
const token = spawnResult.stdout.toString('utf8').trimEnd()
|
||||
if (!token) {
|
||||
throw new PnpmError('TOKEN_HELPER_EMPTY_TOKEN', `Token helper "${helperPath}", configured as ${settingName}, returned an empty token`)
|
||||
}
|
||||
// If the token already contains an auth scheme (e.g. "Bearer ...", "Basic ..."),
|
||||
// return it as-is.
|
||||
if (/^[A-Z]+ /i.test(token)) {
|
||||
return token
|
||||
}
|
||||
return `Bearer ${token}`
|
||||
}
|
||||
|
||||
@@ -10,6 +10,16 @@ const osTokenHelper = {
|
||||
win32: path.join(import.meta.dirname, 'utils/test-exec.bat'),
|
||||
}
|
||||
|
||||
const osRawTokenHelper = {
|
||||
linux: path.join(import.meta.dirname, 'utils/test-exec-raw-token.js'),
|
||||
win32: path.join(import.meta.dirname, 'utils/test-exec-raw-token.bat'),
|
||||
}
|
||||
|
||||
const osEmptyTokenHelper = {
|
||||
linux: path.join(import.meta.dirname, 'utils/test-exec-empty-token.js'),
|
||||
win32: path.join(import.meta.dirname, 'utils/test-exec-empty-token.bat'),
|
||||
}
|
||||
|
||||
const osErrorTokenHelper = {
|
||||
linux: path.join(import.meta.dirname, 'utils/test-exec-error.js'),
|
||||
win32: path.join(import.meta.dirname, 'utils/test-exec-error.bat'),
|
||||
@@ -112,6 +122,30 @@ describe('getAuthHeadersFromConfig()', () => {
|
||||
}
|
||||
expect(getAuthHeadersFromConfig({ allSettings, userSettings: {} })).toStrictEqual({})
|
||||
})
|
||||
it('should prepend Bearer to raw token from tokenHelper', () => {
|
||||
const userSettings = {
|
||||
'//registry.foobar.eu/:tokenHelper': osRawTokenHelper[osFamily],
|
||||
}
|
||||
expect(getAuthHeadersFromConfig({ allSettings: {}, userSettings })).toStrictEqual({
|
||||
'//registry.foobar.eu/': 'Bearer raw-token-no-scheme',
|
||||
})
|
||||
})
|
||||
it('should not modify token that already has an auth scheme', () => {
|
||||
const userSettings = {
|
||||
'//registry.foobar.eu/:tokenHelper': osTokenHelper[osFamily],
|
||||
}
|
||||
expect(getAuthHeadersFromConfig({ allSettings: {}, userSettings })).toStrictEqual({
|
||||
'//registry.foobar.eu/': 'Bearer token-from-spawn',
|
||||
})
|
||||
})
|
||||
it('should throw an error if the token helper returns an empty token', () => {
|
||||
expect(() => getAuthHeadersFromConfig({
|
||||
allSettings: {},
|
||||
userSettings: {
|
||||
'//reg.com:tokenHelper': osEmptyTokenHelper[osFamily],
|
||||
},
|
||||
})).toThrow('returned an empty token')
|
||||
})
|
||||
})
|
||||
|
||||
function encodeBase64 (s: string) {
|
||||
|
||||
2
network/auth-header/test/utils/test-exec-empty-token.bat
Normal file
2
network/auth-header/test/utils/test-exec-empty-token.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
REM Outputs nothing
|
||||
3
network/auth-header/test/utils/test-exec-empty-token.js
Executable file
3
network/auth-header/test/utils/test-exec-empty-token.js
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Outputs nothing
|
||||
2
network/auth-header/test/utils/test-exec-raw-token.bat
Normal file
2
network/auth-header/test/utils/test-exec-raw-token.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
echo raw-token-no-scheme
|
||||
3
network/auth-header/test/utils/test-exec-raw-token.js
Executable file
3
network/auth-header/test/utils/test-exec-raw-token.js
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
console.log('raw-token-no-scheme')
|
||||
@@ -92,7 +92,9 @@ test('dlx should work with npm_config_save_dev env variable', async () => {
|
||||
})
|
||||
})
|
||||
|
||||
test('parallel dlx calls of the same package', async () => {
|
||||
const testParallel = process.version.startsWith('v25.') ? test.skip : test
|
||||
|
||||
testParallel('parallel dlx calls of the same package', async () => {
|
||||
prepareEmpty()
|
||||
|
||||
// parallel dlx calls without cache
|
||||
|
||||
Reference in New Issue
Block a user