Skip to content

Commit

Permalink
Apply the most strict interpretation for Basic Auth encoding
Browse files Browse the repository at this point in the history
Amends 49d3241 per discussion in
#171 with the most
strict interpretation of required encoding for the HTTP Basic
Authentication user and password.
  • Loading branch information
p2004a committed Feb 9, 2025
1 parent 24ae1c5 commit 267166b
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 32 deletions.
14 changes: 13 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ export class OAuth2Client {
// to be encoded using application/x-www-form-urlencoded for the
// basic auth.
headers.Authorization = 'Basic ' +
btoa(encodeURIComponent(this.settings.clientId) + ':' + encodeURIComponent(this.settings.clientSecret!));
btoa(legacyFormUrlEncode(this.settings.clientId) + ':' + legacyFormUrlEncode(this.settings.clientSecret!));
break;
case 'client_secret_post' :
body.client_id = this.settings.clientId;
Expand Down Expand Up @@ -498,3 +498,15 @@ export function generateQueryString(params: Record<string, undefined | number |
return query.toString();

}

/**
* Encodes string according to the most strict interpretation of RFC 6749 Appendix B.
*
* All non-alphanumeric characters are percent encoded, with exception of space which
* is replaced with '+'.
*/
export function legacyFormUrlEncode(value: string): string {
return encodeURIComponent(value)
.replace(/%20/g, '+')
.replace(/[-_.!~*'()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
}
6 changes: 3 additions & 3 deletions test/authorization-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ describe('authorization-code', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
clientId: 'testClientId',
clientSecret: 'testClientSecret',
});

const result = await client.authorizationCode.getToken({
Expand All @@ -212,7 +212,7 @@ describe('authorization-code', () => {
const request = server.lastRequest();
assert.equal(
request.headers.get('Authorization'),
'Basic ' + btoa('test-client-id:test-client-secret')
'Basic ' + btoa('testClientId:testClientSecret')
);
assert.equal(request.headers.get('Accept'), 'application/json');

Expand Down
30 changes: 15 additions & 15 deletions test/client-credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('client-credentials', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id:10',
clientId: 'testClientId:10',
clientSecret: 'test=client=secret',
});

Expand All @@ -32,7 +32,7 @@ describe('client-credentials', () => {
const request = server.lastRequest();
assert.equal(
request.headers.get('Authorization'),
'Basic ' + btoa('test-client-id%3A10:test%3Dclient%3Dsecret')
'Basic ' + btoa('testClientId%3A10:test%3Dclient%3Dsecret')
);

assert.deepEqual(request.body, {
Expand All @@ -45,8 +45,8 @@ describe('client-credentials', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
clientId: 'testClientId',
clientSecret: 'testClientSecret',
});

const result = await client.clientCredentials({
Expand All @@ -63,7 +63,7 @@ describe('client-credentials', () => {
const request = server.lastRequest();
assert.equal(
request.headers.get('Authorization'),
'Basic ' + btoa('test-client-id:test-client-secret')
'Basic ' + btoa('testClientId:testClientSecret')
);
assert.equal(request.headers.get('Accept'), 'application/json');

Expand All @@ -79,8 +79,8 @@ describe('client-credentials', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
clientId: 'testClientId',
clientSecret: 'testClientSecret',
authenticationMethod: 'client_secret_post',
});

Expand All @@ -94,8 +94,8 @@ describe('client-credentials', () => {
const request = server.lastRequest();

assert.deepEqual(request.body, {
client_id: 'test-client-id',
client_secret: 'test-client-secret',
client_id: 'testClientId',
client_secret: 'testClientSecret',
grant_type: 'client_credentials',
});
});
Expand All @@ -105,8 +105,8 @@ describe('client-credentials', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
clientId: 'testClientId',
clientSecret: 'testClientSecret',
});

const resource = ['https://example/resource1', 'https://example/resource2'];
Expand All @@ -123,7 +123,7 @@ describe('client-credentials', () => {
const request = server.lastRequest();
assert.equal(
request.headers.get('Authorization'),
'Basic ' + btoa('test-client-id:test-client-secret')
'Basic ' + btoa('testClientId:testClientSecret')
);

assert.deepEqual(request.body, {
Expand All @@ -140,7 +140,7 @@ describe('client-credentials', () => {
server: server.url,
tokenEndpoint: '/token',
clientId: 'oauth2-error',
clientSecret: 'test-client-secret',
clientSecret: 'testClientSecret',
authenticationMethod: 'client_secret_post',
});

Expand Down Expand Up @@ -171,7 +171,7 @@ describe('client-credentials', () => {
server: server.url,
tokenEndpoint: '/token',
clientId: 'json-error',
clientSecret: 'test-client-secret',
clientSecret: 'testClientSecret',
authenticationMethod: 'client_secret_post',
});

Expand Down Expand Up @@ -204,7 +204,7 @@ describe('client-credentials', () => {
server: server.url,
tokenEndpoint: '/token',
clientId: 'general-http-error',
clientSecret: 'test-client-secret',
clientSecret: 'testClientSecret',
authenticationMethod: 'client_secret_post',
});

Expand Down
10 changes: 10 additions & 0 deletions test/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as assert from 'node:assert';
import { OAuth2Client } from '../src/index.js';
import { legacyFormUrlEncode } from '../src/client.js';
import { describe, it } from 'node:test';

describe('tokenResponseToOAuth2Token', () => {
Expand Down Expand Up @@ -58,3 +59,12 @@ describe('tokenResponseToOAuth2Token', () => {
assert.equal(caught, true);
});
});

describe('legacyFormUrlEncode', () => {
it('correctly encodes full character set', () => {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ó';
assert.equal(
legacyFormUrlEncode(chars),
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F%3A%3B%3C%3D%3E%3F%40%5B%5C%5D%5E%5F%60%7B%7C%7D%7E+%C3%B3');
});
});
14 changes: 7 additions & 7 deletions test/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ describe('password', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
clientId: 'testClientId',
clientSecret: 'testClientSecret',
});

const result = await client.password({
Expand All @@ -35,7 +35,7 @@ describe('password', () => {
const request = server.lastRequest();
assert.equal(
request.headers.get('Authorization'),
'Basic ' + btoa('test-client-id:test-client-secret')
'Basic ' + btoa('testClientId:testClientSecret')
);
assert.equal(request.headers.get('Accept'), 'application/json');

Expand All @@ -52,7 +52,7 @@ describe('password', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientId: 'testClientId',
authenticationMethod: 'client_secret_post',
});

Expand All @@ -72,7 +72,7 @@ describe('password', () => {
grant_type: 'password',
password: 'password',
username: 'user123',
client_id: 'test-client-id',
client_id: 'testClientId',
});
});

Expand All @@ -82,7 +82,7 @@ describe('password', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientId: 'testClientId',
authenticationMethod: 'client_secret_post',
});
const resource = ['https://example/resource1', 'https://example/resource2'];
Expand All @@ -104,7 +104,7 @@ describe('password', () => {
grant_type: 'password',
password: 'password',
username: 'user123',
client_id: 'test-client-id',
client_id: 'testClientId',
resource,
});
});
Expand Down
12 changes: 6 additions & 6 deletions test/refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ describe('refreshing tokens', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
clientId: 'testClientId',
clientSecret: 'testClientSecret',
});

const result = await client.refreshToken({
Expand All @@ -37,7 +37,7 @@ describe('refreshing tokens', () => {
const request = server.lastRequest();
assert.equal(
request.headers.get('Authorization'),
'Basic ' + btoa('test-client-id:test-client-secret')
'Basic ' + btoa('testClientId:testClientSecret')
);
assert.equal(
request.headers.get('Accept'),
Expand All @@ -57,8 +57,8 @@ describe('refreshing tokens', () => {
const client = new OAuth2Client({
server: server.url,
tokenEndpoint: '/token',
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
clientId: 'testClientId',
clientSecret: 'testClientSecret',
});

const result = await client.refreshToken({
Expand All @@ -75,7 +75,7 @@ describe('refreshing tokens', () => {
const request = server.lastRequest();
assert.equal(
request.headers.get('Authorization'),
'Basic ' + btoa('test-client-id:test-client-secret')
'Basic ' + btoa('testClientId:testClientSecret')
);
assert.equal(
request.headers.get('Accept'),
Expand Down

0 comments on commit 267166b

Please sign in to comment.