Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion interop/__tests__/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@
}
);
const cat = await validator.validate(token, 'mac', {
kid: 'Symmetric256',
issuer: 'eyevinn'
});
expect(cat).toBeDefined();
expect(cat!.claims.iss).toBe('eyevinn');

Check warning on line 37 in interop/__tests__/validate.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
});
});
4 changes: 1 addition & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ const httpValidator = new HttpValidator({

const server = http.createServer((req, res) => {
const result = await httpValidator.validateHttpRequest(
req,
'Symmetric256'
req
);
res.writeHead(result.status, { 'Content-Type': 'text/plain' });
res.end(result.message || 'ok');
Expand Down Expand Up @@ -126,7 +125,6 @@ const base64encoded =
'0YRDoQEEoQRMU3ltbWV0cmljMjU2eKZkOTAxMDNhNzAxNzU2MzZmNjE3MDNhMmYyZjYxNzMyZTY1Nzg2MTZkNzA2YzY1MmU2MzZmNmQwMjY1NmE2ZjZlNjE3MzAzNzgxODYzNmY2MTcwM2EyZjJmNmM2OTY3Njg3NDJlNjU3ODYxNmQ3MDZjNjUyZTYzNmY2ZDA0MWE1NjEyYWViMDA1MWE1NjEwZDlmMDA2MWE1NjEwZDlmMDA3NDIwYjcxSKuCk/+kFmlY';
try {
const cat = await validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'coap://as.example.com'
});
console.log(cat.claims);
Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { CommonAccessTokenValue } from './cat';

export class KeyNotFoundError extends Error {
constructor() {
super(`Failed to validate token signature with any of the available keys`);
}
}

export class InvalidClaimTypeError extends Error {
constructor(claim: string, value: CommonAccessTokenValue) {
super(`Invalid claim type for ${claim}: ${typeof value}`);
Expand Down
74 changes: 66 additions & 8 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
InvalidAudienceError,
InvalidIssuerError,
KeyNotFoundError,
TokenExpiredError
} from './errors';

Expand Down Expand Up @@ -33,12 +34,11 @@
)
}
});
const cat = await validator.validate(base64encoded!, 'mac', {

Check warning on line 37 in src/index.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
kid: 'Symmetric256',
issuer: 'coap://as.example.com'
});
expect(cat).toBeDefined();
expect(cat!.claims).toEqual({

Check warning on line 41 in src/index.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
iss: 'coap://as.example.com'
});
});
Expand All @@ -55,11 +55,10 @@
}
});
const cat = await validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'coap://as.example.com'
});
expect(cat).toBeDefined();
expect(cat!.claims).toEqual({

Check warning on line 61 in src/index.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
iss: 'coap://as.example.com'
});
});
Expand All @@ -77,16 +76,80 @@
expectCwtTag: true
});
const cat = await validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'coap://jonas.example.com'
});
expect(cat).toBeDefined();
expect(cat!.claims).toEqual({

Check warning on line 82 in src/index.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
iss: 'coap://jonas.example.com',
iat: 1741985961,
nbf: 1741985961
});
});

test('can handle multiple keys (under rotation)', async () => {
const validator = new CAT({
keys: {
keyone: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
),
keytwo: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569389',
'hex'
)
},
expectCwtTag: true
});

const generator1 = new CAT({
keys: {
keyone: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
},
expectCwtTag: true
});
const token1 = await generator1.generate(
{
iss: 'coap://as.example.com'
},
{
type: 'mac',
alg: 'HS256',
kid: 'keyone'
}
);
const cat = await validator.validate(token1!, 'mac', {

Check warning on line 123 in src/index.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
issuer: 'coap://as.example.com'
});
expect(cat).toBeDefined();

const generator2 = new CAT({
keys: {
keythree: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569398',
'hex'
)
},
expectCwtTag: true
});
const token2 = await generator2.generate(
{
iss: 'coap://as.example.com'
},
{
type: 'mac',
alg: 'HS256',
kid: 'keythree'
}
);
await expect(
validator.validate(token2!, 'mac', {

Check warning on line 148 in src/index.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
issuer: 'coap://as.example.com'
})
).rejects.toThrow(KeyNotFoundError);
});
});

describe('CAT claims', () => {
Expand All @@ -109,7 +172,6 @@
'2D3RhEOhAQWhBFBha2FtYWlfa2V5X2hzMjU2VKMBZWpvbmFzBhpn12U-BRpn12U-WCDf1xHvhcnvyXUxd-DP4RAbayc8nC2PLJPPPbF3S00ruw';
await expect(
validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'coap://jonas.example.com'
})
).rejects.toThrow(InvalidIssuerError);
Expand All @@ -119,7 +181,6 @@
const base64encoded =
'2D3RhEOhAQWhBFBha2FtYWlfa2V5X2hzMjU2U6MEGnUCOrsGGmfXRKwFGmfXRKxYIOM6yRx830uqAamWFv1amFYRa5vaV2z5lIQTqFEvFh8z';
const cat = validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'eyevinn'
});
expect(cat).toBeDefined();
Expand All @@ -130,7 +191,6 @@
'2D3RhEOhAQWhBFBha2FtYWlfa2V5X2hzMjU2U6MEGmfXP_YGGmfXQAsFGmfXQAtYINTT_KlOyhaV6NaSxFXkqJWfBagSkPkem10dysoA-C0w';
await expect(
validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'eyevinn'
})
).rejects.toThrow(TokenExpiredError);
Expand All @@ -141,7 +201,6 @@
const base64encoded =
'2D3RhEOhAQWhBFBha2FtYWlfa2V5X2hzMjU2V6MDgmNvbmVjdHdvBhpn12R8BRpn12R8WCAdnSbUN4KbIvaHLn-q4f4YRpfq6ERYotByjbIyZ-EkfQ';
const cat = validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'eyevinn',
audience: ['one', 'three']
});
Expand All @@ -154,7 +213,6 @@
'2D3RhEOhAQWhBFBha2FtYWlfa2V5X2hzMjU2V6MDgmNvbmVjdHdvBhpn12R8BRpn12R8WCAdnSbUN4KbIvaHLn-q4f4YRpfq6ERYotByjbIyZ-EkfQ';
await expect(
validator.validate(base64encoded, 'mac', {
kid: 'Symmetric256',
issuer: 'eyevinn',
audience: ['three']
})
Expand Down
39 changes: 27 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CommonAccessToken, CommonAccessTokenFactory } from './cat';
import { KeyNotFoundError } from './errors';

export { CommonAccessToken } from './cat';
export { HttpValidator } from './validators/http';
Expand All @@ -7,7 +8,6 @@ export type CatValidationTypes = 'mac' | 'sign' | 'none';

export interface CatValidationOptions {
alg?: string;
kid: string;
issuer: string;
audience?: string[];
}
Expand Down Expand Up @@ -80,18 +80,33 @@ export class CAT {
if (!opts) {
throw new Error('Missing options for MAC validation');
}
const key = this.keys[opts.kid];
if (!key) {
throw new Error('Key not found');
let error;
for (const kid in this.keys) {
try {
const key = this.keys[kid];
cat = await CommonAccessTokenFactory.fromMacedToken(
tokenWithoutPadding,
{
k: key,
kid: kid
},
this.expectCwtTag
);
if (cat && cat.claims) {
error = undefined;
break;
}
} catch (err) {
error = err;
}
}
if (error) {
if ((error as Error).message === 'Tag mismatch') {
throw new KeyNotFoundError();
} else {
throw error;
}
}
cat = await CommonAccessTokenFactory.fromMacedToken(
tokenWithoutPadding,
{
k: key,
kid: opts.kid
},
this.expectCwtTag
);
} else {
throw new Error('Unsupported validation type');
}
Expand Down
69 changes: 56 additions & 13 deletions src/validators/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createRequest } from 'node-mocks-http';
import { HttpValidator } from './http';
import { CAT } from '..';

describe('HTTP Request CAT Validator', () => {
test('fail to validate token in CTA-Common-Access-Token header with wrong signature', async () => {
Expand All @@ -22,13 +23,12 @@ describe('HTTP Request CAT Validator', () => {
],
issuer: 'eyevinn'
});
const result = await httpValidator.validateHttpRequest(
request,
'Symmetric256'
);
const result = await httpValidator.validateHttpRequest(request);
expect(result.status).not.toBe(200);
expect(result.status).toBe(401);
expect(result.message).toBe('Tag mismatch');
expect(result.message).toBe(
'Failed to validate token signature with any of the available keys'
);
});

test('can validate token in CTA-Common-Access-Token header', async () => {
Expand All @@ -51,10 +51,7 @@ describe('HTTP Request CAT Validator', () => {
],
issuer: 'eyevinn'
});
const result = await httpValidator.validateHttpRequest(
request,
'Symmetric256'
);
const result = await httpValidator.validateHttpRequest(request);
expect(result.status).toBe(200);
});

Expand All @@ -78,10 +75,56 @@ describe('HTTP Request CAT Validator', () => {
],
issuer: 'eyevinn'
});
const result = await httpValidator.validateHttpRequest(
request,
'Symmetric256'
);
const result = await httpValidator.validateHttpRequest(request);
expect(result.status).toBe(401);
});

test('can handle multiple keys (under rotation)', async () => {
const generator = new CAT({
keys: {
keyone: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
},
expectCwtTag: true
});
const token = await generator.generate(
{
iss: 'eyevinn'
},
{
type: 'mac',
alg: 'HS256',
kid: 'keyone'
}
);
const request = createRequest({
method: 'GET',
headers: {
'CTA-Common-Access-Token': token
}
});
const httpValidator = new HttpValidator({
keys: [
{
kid: 'keyon',
key: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
},
{
kid: 'keytwo',
key: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569389',
'hex'
)
}
],
issuer: 'eyevinn'
});
const result = await httpValidator.validateHttpRequest(request);
expect(result.status).toBe(200);
});
});
4 changes: 1 addition & 3 deletions src/validators/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ export class HttpValidator {
}

public async validateHttpRequest(
request: IncomingMessage,
kid: string
request: IncomingMessage
): Promise<HttpResponse> {
const validator = new CAT({
keys: this.keys
Expand All @@ -64,7 +63,6 @@ export class HttpValidator {
const token = request.headers['cta-common-access-token'] as string;
try {
await validator.validate(token, 'mac', {
kid,
issuer: this.opts.issuer,
audience: this.opts.audience
});
Expand Down
Loading