Skip to content
2 changes: 1 addition & 1 deletion examples/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const httpValidator = new HttpValidator({
});

const server = http.createServer(async (req, res) => {
const result = await httpValidator.validateHttpRequest(req, 'Symmetric256');
const result = await httpValidator.validateHttpRequest(req, res);
res.writeHead(result.status, { 'Content-Type': 'text/plain' });
res.end(result.message || 'ok');
});
Expand Down
21 changes: 16 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@

</div>

This is a Node library for generating and validating Common Access Tokens (CTA-5007)
This is a Node library for generating and validating Common Access Tokens (CTA-5007).

Features:

- Generate and Validate Common Access Tokens. Supported claims in table below.
- HTTP and CloudFront Lambda handlers supporting
- Validation and parsing of tokens
- Handle automatic renewal of tokens

## Claims Validation Support

Expand All @@ -46,6 +53,7 @@ This is a Node library for generating and validating Common Access Tokens (CTA-5
| Geohash (`geohash`) | No |
| Common Access Token Altitude (`catgeoalt`) | No |
| Common Access Token TLS Public Key (`cattpk`) | No |
| Common ACcess Token Renewal (`catr`) claim | Yes |

## Requirements

Expand All @@ -72,16 +80,18 @@ const httpValidator = new HttpValidator({
)
}
],
autoRenewEnabled: true // Token renewal enabled. Optional (default: true)
tokenMandatory: true // Optional (default: true)
issuer: 'eyevinn',
audience: ['one', 'two'] // Optional
});

const server = http.createServer((req, res) => {
const result = await httpValidator.validateHttpRequest(
req
req, res
);
console.log(result.claims);
console.log(result.claims); // Claims
console.log(res.getHeaders('cta-common-access-token')); // Renewed token
res.writeHead(result.status, { 'Content-Type': 'text/plain' });
res.end(result.message || 'ok');
});
Expand Down Expand Up @@ -140,8 +150,9 @@ export const handler = async (
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;
// If renewed new token is found here given catr type is "header"
console.log(result.cfResponse.headers['cta-common-access-token']);
response = result.cfResponse;
if (result.claims) {
console.log(result.claims);
}
Expand Down
76 changes: 76 additions & 0 deletions src/cat.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CommonAccessToken, CommonAccessTokenFactory } from './cat';
import { CommonAccessTokenRenewal } from './catr';

describe('CAT', () => {
test('can create a CAT object and return claims as JSON', () => {
Expand Down Expand Up @@ -32,7 +33,7 @@
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 36 in src/cat.test.ts

View workflow job for this annotation

GitHub Actions / lint

Forbidden non-null assertion
const newCat = new CommonAccessToken({});
await newCat.parse(token, key, { expectCwtTag: true });
expect(newCat.claims).toEqual(claims);
Expand All @@ -58,7 +59,7 @@
};
await cat.sign(signKey, 'ES256');
const signedHex = cat.raw?.toString('hex');
const token = Buffer.from(signedHex!, 'hex');

Check warning on line 62 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 Down Expand Up @@ -128,4 +129,79 @@
cti: '0b71'
});
});

test('can provide information about renewal mechanism', async () => {
const cat = new CommonAccessToken({
iss: 'eyevinn',
catr: CommonAccessTokenRenewal.fromDict({
type: 'header',
'header-name': 'cta-common-access-token'
}).payload
});
expect(cat.claims.catr).toEqual({
type: 'header',
'header-name': 'cta-common-access-token'
});
});

test('can create a CAT object from a dict', async () => {
const cat = CommonAccessTokenFactory.fromDict({
iss: 'eyevinn',
sub: 'jonas',
aud: 'coap://light.example.com',
exp: 1444064944,
nbf: 1443944944,
iat: 1443944944,
cti: '0b71',
catr: {
type: 'header',
'header-name': 'cta-common-access-token'
}
});
expect(cat.claims).toEqual({
iss: 'eyevinn',
sub: 'jonas',
aud: 'coap://light.example.com',
exp: 1444064944,
nbf: 1443944944,
iat: 1443944944,
cti: '0b71',
catr: {
type: 'header',
'header-name': 'cta-common-access-token'
}
});
});

test('can determine whether the token should be renewed', async () => {
const now = Math.floor(Date.now() / 1000);
const cat = new CommonAccessToken({
iss: 'eyevinn',
exp: now + 30,
catr: CommonAccessTokenRenewal.fromDict({
type: 'automatic',
expadd: 60
}).payload
});
expect(cat.shouldRenew).toBe(true);
const cat2 = new CommonAccessToken({
iss: 'eyevinn',
exp: now + 100,
catr: CommonAccessTokenRenewal.fromDict({
type: 'automatic',
expadd: 60
}).payload
});
expect(cat2.shouldRenew).toBe(false);
const cat3 = new CommonAccessToken({
iss: 'eyevinn',
exp: now + 100,
catr: CommonAccessTokenRenewal.fromDict({
type: 'automatic',
expadd: 60,
deadline: 105
}).payload
});
expect(cat3.shouldRenew).toBe(true);
});
});
73 changes: 70 additions & 3 deletions src/cat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
InvalidAudienceError,
InvalidClaimTypeError,
InvalidIssuerError,
RenewalClaimError,
TokenExpiredError,
TokenNotActiveError,
UriNotAllowedError
} from './errors';
import { CatValidationOptions } from '.';
import { CommonAccessTokenUri } from './catu';
import { CommonAccessTokenRenewal } from './catr';

const claimsToLabels: { [key: string]: number } = {
iss: 1, // 3
Expand Down Expand Up @@ -83,16 +85,16 @@
const CWT_TAG = 61;

export type CommonAccessTokenClaims = {
[key: string]: string | number | Map<number, any>;

Check warning on line 88 in src/cat.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
};
export type CommonAccessTokenDict = {
[key: string]: string | number | { [key: string]: any };

Check warning on line 91 in src/cat.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
};
export type CommonAccessTokenValue =
| string
| number
| Buffer
| Map<number, any>;

Check warning on line 97 in src/cat.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

export interface CWTEncryptionKey {
k: Buffer;
Expand Down Expand Up @@ -131,9 +133,34 @@
return map;
}

function updateMapFromDict(
dict: CommonAccessTokenDict
): CommonAccessTokenClaims {
const claims: CommonAccessTokenClaims = {};
for (const param in dict) {
const key = claimsToLabels[param];
if (param === 'catu') {
claims[key] = CommonAccessTokenUri.fromDict(
dict[param] as { [key: string]: any }

Check warning on line 144 in src/cat.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
).payload;
} else if (param === 'catr') {
claims[key] = CommonAccessTokenRenewal.fromDict(
dict[param] as { [key: string]: any }

Check warning on line 148 in src/cat.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
).payload;
} else {
const value = claimTransform[param]
? claimTransform[param](dict[param] as string)
: dict[param];
claims[key] = value as string | number;
}
}
return claims;
}

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

constructor(claims: CommonAccessTokenClaims) {
this.payload = updateMapFromClaims(claims);
Expand Down Expand Up @@ -168,6 +195,7 @@
const plaintext = cbor.encode(this.payload).toString('hex');
this.data = await cose.mac.create(headers, plaintext, recipient);
}
this.kid = key.kid;
}

public async parse(
Expand All @@ -190,6 +218,7 @@
const buf = await cose.mac.read(token, key.k);
this.payload = await cbor.decode(Buffer.from(buf.toString('hex'), 'hex'));
}
this.kid = key.kid;
}

public async sign(key: CWTSigningKey, alg: string): Promise<void> {
Expand Down Expand Up @@ -257,7 +286,7 @@
}
if (this.payload.get(claimsToLabels['catu'])) {
const catu = CommonAccessTokenUri.fromMap(
this.payload.get(claimsToLabels['catu']) as Map<number, any>

Check warning on line 289 in src/cat.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
);
if (!opts.url) {
throw new UriNotAllowedError('No URL provided');
Expand All @@ -266,13 +295,38 @@
throw new UriNotAllowedError(`URI ${opts.url} not allowed`);
}
}
if (this.payload.get(claimsToLabels['catr'])) {
const catr = CommonAccessTokenRenewal.fromMap(
this.payload.get(claimsToLabels['catr']) as Map<number, any>
);
if (!catr.isValid()) {
throw new RenewalClaimError('Invalid renewal claim');
}
}

return true;
}

get(key: string) {
const theKey = claimsToLabels[key] ? claimsToLabels[key] : parseInt(key);
return this.payload.get(theKey);
get shouldRenew(): boolean {
const exp = this.payload.get(claimsToLabels['exp']) as number;
if (exp) {
const catr = this.payload.get(claimsToLabels['catr']);
if (catr) {
const renewal = CommonAccessTokenRenewal.fromMap(
catr as Map<number, any>
).toDict();
const now = Math.floor(Date.now() / 1000);
let lowThreshold = exp - 1 * 60;
if (renewal.deadline !== undefined) {
lowThreshold = exp - renewal.deadline;
}
//console.log(`${now} >= ${lowThreshold} && ${now} < ${exp}`);
if (now >= lowThreshold && now < exp) {
return true;
}
}
}
return false;
}

get claims(): CommonAccessTokenDict {
Expand All @@ -283,6 +337,10 @@
result[key] = CommonAccessTokenUri.fromMap(
value as Map<number, any>
).toDict();
} else if (key === 'catr') {
result[key] = CommonAccessTokenRenewal.fromMap(
value as Map<number, any>
).toDict();
} else {
const theValue = claimTransformReverse[key]
? claimTransformReverse[key](value as Buffer)
Expand All @@ -300,6 +358,10 @@
get base64() {
return this.data?.toString('base64');
}

get keyId() {
return this.kid;
}
}

export class CommonAccessTokenFactory {
Expand All @@ -323,4 +385,9 @@
await cat.parse(token, key, { expectCwtTag });
return cat;
}

public static fromDict(claims: CommonAccessTokenDict) {
const cat = new CommonAccessToken(updateMapFromDict(claims));
return cat;
}
}
106 changes: 106 additions & 0 deletions src/catr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
type CatrPart =
| 'type'
| 'expadd'
| 'deadline'
| 'cookie-name'
| 'header-name'
| 'cookie-params'
| 'header-params'
| 'code';

type CatrRenewalType = 'automatic' | 'cookie' | 'header' | 'redirect';

const catrPartToLabel: { [key: string]: number } = {
type: 0,
expadd: 1,
deadline: 2,
'cookie-name': 3,
'header-name': 4,
'cookie-params': 5,
'header-params': 6,
code: 7
};

const labelsToCatrPart: { [key: number]: CatrPart } = {
0: 'type',
1: 'expadd',
2: 'deadline',
3: 'cookie-name',
4: 'header-name',
5: 'cookie-params',
6: 'header-params',
7: 'code'
};

const catrRenewalTypeToLabel: { [key: string]: number } = {
automatic: 0,
cookie: 1,
header: 2,
redirect: 3
};
const labelsToCatrRenewalType: { [key: number]: CatrRenewalType } = {
0: 'automatic',
1: 'cookie',
2: 'header',
3: 'redirect'
};

type CatrPartValue = number | string | string[];
export type CommonAccessTokenRenewalMap = Map<number, CatrPartValue>;

export class CommonAccessTokenRenewal {
private catrMap: CommonAccessTokenRenewalMap = new Map();

public static fromDict(dict: { [key: string]: any }) {
const catr = new CommonAccessTokenRenewal();
for (const catrPart in dict) {
if (catrPart === 'type') {
catr.catrMap.set(
catrPartToLabel[catrPart],
catrRenewalTypeToLabel[dict[catrPart]]
);
} else {
catr.catrMap.set(catrPartToLabel[catrPart], dict[catrPart]);
}
}
return catr;
}

public static fromMap(map: CommonAccessTokenRenewalMap) {
const catr = new CommonAccessTokenRenewal();
catr.catrMap = map;
return catr;
}

toDict() {
const result: { [key: string]: any } = {};
for (const [key, value] of this.catrMap.entries()) {
if (labelsToCatrPart[key] === 'type') {
result[labelsToCatrPart[key]] =
labelsToCatrRenewalType[value as number];
} else {
result[labelsToCatrPart[key]] = value;
}
}
return result;
}

isValid() {
if (this.catrMap.get(catrPartToLabel['type']) === undefined) {
return false;
}
if (this.catrMap.get(catrPartToLabel['expadd']) === undefined) {
return false;
}
return true;
}

get renewalType(): CatrRenewalType {
const type = this.catrMap.get(catrPartToLabel['type']);
return labelsToCatrRenewalType[type as number];
}

get payload() {
return this.catrMap;
}
}
Loading
Loading