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
28 changes: 14 additions & 14 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ This is a Node library for generating and validating Common Access Tokens (CTA-5
| Issuer (`iss`) | Yes |
| Audience (`aud`) | Yes |
| Expiration (`exp`) | Yes |
| Not Before (`nbf`) | No |
| Not Before (`nbf`) | Yes |
| CWT ID (`cti`) | No |
|  Common Access Token Replay (`catreplay`) | No |
| Common Access Token Replay (`catreplay`) | No |
| Common Access Token Probability of Rejection (`catpor`) | No |
| Common Access Token Version (`catv`) | No |
| Common Access Token Network IP (`catnip`) |  No |
| Common Access Token Network IP (`catnip`) | No |
| Common Access Token URI (`catu`) | No |
| Common Access Token Methods (`catm`) | No |
| Common Access Token ALPN (`catalpn`) |  No |
|  Common Access Token Header (`cath`) | No |
| Common Access Token Geographic ISO3166 (`catgeoiso3166`) |  No |
| Common Access Token ALPN (`catalpn`) | No |
| Common Access Token Header (`cath`) | No |
| Common Access Token Geographic ISO3166 (`catgeoiso3166`) | No |
| Common Access Token Geographic Coordinate (`catgeocoord`) | No |
|  Geohash (`geohash`) | No |
|  Common Access Token Altitude (`catgeoalt`) |  No |
|  Common Access Token TLS Public Key (`cattpk`) | No |
| Geohash (`geohash`) | No |
| Common Access Token Altitude (`catgeoalt`) | No |
| Common Access Token TLS Public Key (`cattpk`) | No |

## Requirements

Expand Down Expand Up @@ -115,11 +115,11 @@ Token has expired
import {
Context,
CloudFrontResponseEvent,
CloudFrontResponseCallback,
} from "aws-lambda";
CloudFrontResponseCallback
} from 'aws-lambda';
import { HttpValidator } from '@eyevinn/cat';

export const handler = async(
export const handler = async (
event: CloudFrontResponseEvent,
context: Context,
callback: CloudFrontResponseCallback
Expand All @@ -134,15 +134,15 @@ export const handler = async(
)
}
],
issuer: 'eyevinn',
issuer: 'eyevinn'
});
const request = event.Records[0].cf.request;
const response = event.Records[0].cf.response;
const result = await httpValidator.validateCloudFrontRequest(request);
response.status = result.status;
response.statusDescription = result.message;
callback(null, response);
}
};
```

### Verify token
Expand Down
19 changes: 10 additions & 9 deletions src/cat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@
),
kid: 'Symmetric256'
};
const mac = await cat.mac(key, 'HS256', { addCwtTag: true });
expect(mac.raw).toBeDefined();
const macHex = mac.raw?.toString('hex');
await cat.mac(key, 'HS256', { addCwtTag: true });
expect(cat.raw).toBeDefined();
const macHex = cat.raw?.toString('hex');
const token = Buffer.from(macHex!, 'hex');

Check warning on line 35 in src/cat.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
const parsed = await cat.parse(token, key, { expectCwtTag: true });
expect(parsed.claims).toEqual(claims);
const newCat = new CommonAccessToken({});
await newCat.parse(token, key, { expectCwtTag: true });
expect(newCat.claims).toEqual(claims);
});

test('can sign a CAT object', async () => {
Expand All @@ -55,9 +56,9 @@
),
kid: 'AsymmetricECDSA256'
};
const signed = await cat.sign(signKey, 'ES256');
const signedHex = signed.raw?.toString('hex');
await cat.sign(signKey, 'ES256');
const signedHex = cat.raw?.toString('hex');
const token = Buffer.from(signedHex!, 'hex');

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

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
const verifyKey = {
x: Buffer.from(
'143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f',
Expand All @@ -69,8 +70,8 @@
),
kid: 'AsymmetricECDSA256'
};
const verified = await cat.verify(token, verifyKey);
expect(verified.claims).toEqual(claims);
await cat.verify(token, verifyKey);
expect(cat.claims).toEqual(claims);
});

test('can create a CAT object from a signed base64 encoded token', async () => {
Expand Down
56 changes: 32 additions & 24 deletions src/cat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
InvalidAudienceError,
InvalidClaimTypeError,
InvalidIssuerError,
TokenExpiredError
TokenExpiredError,
TokenNotActiveError
} from './errors';
import { CatValidationOptions } from '.';

Expand Down Expand Up @@ -73,7 +74,8 @@ const claimTransformReverse: { [key: string]: (value: Buffer) => string } = {
const claimTypeValidators: { [key: string]: (value: string) => boolean } = {
iss: (value) => typeof value === 'string',
exp: (value) => typeof value === 'number',
aud: (value) => typeof value === 'string' || Array.isArray(value)
aud: (value) => typeof value === 'string' || Array.isArray(value),
nbf: (value) => typeof value === 'number'
};

const CWT_TAG = 61;
Expand All @@ -100,25 +102,30 @@ export interface CWTVerifierKey {
}

function updateMapFromClaims(
map: Map<number, CommonAccessTokenValue>,
claims: CommonAccessTokenClaims
) {
for (const param in claims) {
): Map<number, CommonAccessTokenValue> {
const map = new Map<number, CommonAccessTokenValue>();

let dict = claims;
if (claims instanceof Map) {
dict = Object.fromEntries(claims);
}
for (const param in dict) {
const key = claimsToLabels[param] ? claimsToLabels[param] : parseInt(param);
const value = claimTransform[param]
? claimTransform[param](claims[param] as string)
: claims[param];
? claimTransform[param](dict[param] as string)
: dict[param];
map.set(key, value);
}
return map;
}

export class CommonAccessToken {
private payload: Map<number, CommonAccessTokenValue>;
private data?: Buffer;

constructor(claims: CommonAccessTokenClaims) {
this.payload = new Map<number, CommonAccessTokenValue>();
updateMapFromClaims(this.payload, claims);
this.payload = updateMapFromClaims(claims);
}

public async mac(
Expand All @@ -127,7 +134,7 @@ export class CommonAccessToken {
opts?: {
addCwtTag: boolean;
}
): Promise<CommonAccessToken> {
): Promise<void> {
const headers = {
p: { alg: alg },
u: { kid: key.kid }
Expand All @@ -150,7 +157,6 @@ export class CommonAccessToken {
const plaintext = cbor.encode(this.payload).toString('hex');
this.data = await cose.mac.create(headers, plaintext, recipient);
}
return this;
}

public async parse(
Expand All @@ -159,7 +165,7 @@ export class CommonAccessToken {
opts?: {
expectCwtTag: boolean;
}
): Promise<CommonAccessToken> {
): Promise<void> {
const coseMessage = cbor.decode(token);
if (opts?.expectCwtTag && coseMessage.tag !== 61) {
throw new Error('Expected CWT tag');
Expand All @@ -168,18 +174,14 @@ export class CommonAccessToken {
const cborCoseMessage = cbor.encode(coseMessage.value);
const buf = await cose.mac.read(cborCoseMessage, key.k);
const json = await cbor.decode(buf);
updateMapFromClaims(this.payload, json);
this.payload = updateMapFromClaims(json);
} else {
const buf = await cose.mac.read(token, key.k);
this.payload = await cbor.decode(Buffer.from(buf.toString('hex'), 'hex'));
}
return this;
}

public async sign(
key: CWTSigningKey,
alg: string
): Promise<CommonAccessToken> {
public async sign(key: CWTSigningKey, alg: string): Promise<void> {
const plaintext = cbor.encode(this.payload).toString('hex');
const headers = {
p: { alg: alg },
Expand All @@ -189,7 +191,6 @@ export class CommonAccessToken {
key: key
};
this.data = await cose.sign.create(headers, plaintext, signer);
return this;
}

public async verify(
Expand All @@ -204,7 +205,7 @@ export class CommonAccessToken {
private async validateTypes() {
for (const [key, value] of this.payload) {
const claim = labelsToClaim[key];
if (claimTypeValidators[claim]) {
if (value && claimTypeValidators[claim]) {
if (!claimTypeValidators[claim](value as string)) {
throw new InvalidClaimTypeError(claim, value as string);
}
Expand Down Expand Up @@ -236,6 +237,13 @@ export class CommonAccessToken {
}
}
}
if (
this.payload.get(claimsToLabels['nbf']) &&
(this.payload.get(claimsToLabels['nbf']) as number) >
Math.floor(Date.now() / 1000)
) {
throw new TokenNotActiveError();
}
return true;
}

Expand Down Expand Up @@ -272,8 +280,8 @@ export class CommonAccessTokenFactory {
): Promise<CommonAccessToken> {
const token = Buffer.from(base64encoded, 'base64');
const cat = new CommonAccessToken({});
const verified = await cat.verify(token, key);
return verified;
await cat.verify(token, key);
return cat;
}

public static async fromMacedToken(
Expand All @@ -283,7 +291,7 @@ export class CommonAccessTokenFactory {
): Promise<CommonAccessToken> {
const token = Buffer.from(base64encoded, 'base64');
const cat = new CommonAccessToken({});
const parsed = await cat.parse(token, key, { expectCwtTag });
return parsed;
await cat.parse(token, key, { expectCwtTag });
return cat;
}
}
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ export class InvalidAudienceError extends Error {
super(`Invalid audience: ${audience.join(', ')}`);
}
}

export class TokenNotActiveError extends Error {
constructor() {
super('Token is not yet active');
}
}
52 changes: 51 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
InvalidAudienceError,
InvalidIssuerError,
KeyNotFoundError,
TokenExpiredError
TokenExpiredError,
TokenNotActiveError
} from './errors';

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

Check warning on line 38 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();
expect(cat!.claims).toEqual({

Check warning on line 42 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 @@ -58,7 +59,7 @@
issuer: 'coap://as.example.com'
});
expect(cat).toBeDefined();
expect(cat!.claims).toEqual({

Check warning on line 62 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 @@ -79,7 +80,7 @@
issuer: 'coap://jonas.example.com'
});
expect(cat).toBeDefined();
expect(cat!.claims).toEqual({

Check warning on line 83 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
Expand Down Expand Up @@ -120,7 +121,7 @@
kid: 'keyone'
}
);
const cat = await validator.validate(token1!, 'mac', {

Check warning on line 124 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();
Expand All @@ -145,7 +146,7 @@
}
);
await expect(
validator.validate(token2!, 'mac', {

Check warning on line 149 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);
Expand All @@ -154,6 +155,7 @@

describe('CAT claims', () => {
let validator: CAT;
let generator: CAT;

beforeEach(() => {
validator = new CAT({
Expand All @@ -165,6 +167,15 @@
},
expectCwtTag: true
});
generator = new CAT({
keys: {
Symmetric256: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
},
expectCwtTag: true
});
});

test('fail if wrong issuer', async () => {
Expand Down Expand Up @@ -218,4 +229,43 @@
})
).rejects.toThrow(InvalidAudienceError);
});

test('fail if token is not active yet', async () => {
const nbf = Math.floor(Date.now() / 1000) + 1000;
const base64encoded = await generator.generate(
{
iss: 'eyevinn',
nbf
},
{
type: 'mac',
alg: 'HS256',
kid: 'Symmetric256'
}
);
await expect(
validator.validate(base64encoded!, 'mac', {
issuer: 'eyevinn'
})
).rejects.toThrow(TokenNotActiveError);
});

test('pass if token is active', async () => {
const nbf = Math.floor(Date.now() / 1000) - 1000;
const base64encoded = await generator.generate(
{
iss: 'eyevinn',
nbf
},
{
type: 'mac',
alg: 'HS256',
kid: 'Symmetric256'
}
);
const cat = await validator.validate(base64encoded!, 'mac', {
issuer: 'eyevinn'
});
expect(cat).toBeDefined();
});
});
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,13 @@ export class CAT {
if (!key) {
throw new Error('Key not found');
}
const mac = await cat.mac({ k: key, kid: opts.kid }, opts.alg, {
await cat.mac({ k: key, kid: opts.kid }, opts.alg, {
addCwtTag: this.expectCwtTag
});
if (!mac.raw) {
if (!cat.raw) {
throw new Error('Failed to MAC token');
}
return mac.raw.toString('base64');
return cat.raw.toString('base64');
}
}
}
Loading