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
51 changes: 49 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Features:
| Geohash (`geohash`) | Yes | No |
| Common Access Token Altitude (`catgeoalt`) | Yes | No |
| Common Access Token TLS Public Key (`cattpk`) | Yes | No |
| Common Access Token If (`catif`) claim | Yes | No |
| Common Access Token If (`catif`) claim | Yes | Yes |
| Common Access Token Renewal (`catr`) claim | Yes | No |

## Requirements
Expand Down Expand Up @@ -204,7 +204,7 @@ try {
### Generate token

```javascript
import { CAT, CommonAccessTokenRenewal } from '@eyevinn/cat';
import { CAT } from '@eyevinn/cat';

const generator = new CAT({
keys: {
Expand Down Expand Up @@ -238,6 +238,53 @@ const base64encoded = await generator.generateFromJson(
);
```

This example generates a token with a `catif` claim.

```javascript
import { CAT } from '@eyevinn/cat';

const generator = new CAT({
keys: {
Symmetric256: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
}
});

const json = {
iss: 'eyevinn',
exp: Math.floor(Date.now() / 1000) - 60,
cti: 'foobar',
catif: {
exp: [
307,
{
Location: [
'https://auth.example.net/?CAT=',
{
iss: null,
iat: null,
catu: {
host: { 'exact-match': 'auth.example.net' }
}
}
]
},
'Symmetric256'
]
}
};
const base64encoded = await generator.generateFromJson(json, {
type: 'mac',
alg: 'HS256',
kid: 'Symmetric256',
generateCwtId: true
});
```

Providing above token to the `HttpValidator` the validator will return with a status code `307` and the header `Location` with the value of `https://auth.example.net/?CAT=<newtoken>` where `<newtoken>` is created by the JSON specified in the `catif` claim.

## Token store plugins

To enable token usage count the HTTP validators requires a way to store the token id:s that has been used. The following types of stores are supported today.
Expand Down
48 changes: 48 additions & 0 deletions src/catif.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CommonAccessTokenIf } from './catif';

describe('Common Access Token If', () => {
test('can be constructed from a dict', async () => {
const basic = CommonAccessTokenIf.fromDict({
exp: [
307,
{
Location: 'https://auth.example.net/'
}
]
});
expect(basic.toDict()).toEqual({
exp: [307, { Location: 'https://auth.example.net/' }]
});

const advanced = CommonAccessTokenIf.fromDict({
exp: [
307,
{
Location: [
'https://auth.example.net/?CAT=',
{
iss: null,
iat: null
}
]
},
'mykey'
]
});
expect(advanced.toDict()).toEqual({
exp: [
307,
{
Location: [
'https://auth.example.net/?CAT=',
{
iss: null,
iat: null
}
]
},
'mykey'
]
});
});
});
10 changes: 9 additions & 1 deletion src/catif.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { claimsToLabels, labelsToClaim } from './cat';
import { claimsToLabels, CommonAccessTokenDict, labelsToClaim } from './cat';

type CatIfValue = Map<number, [number, { [key: string]: string }]>;
export type CommonAccessTokenIfMap = Map<number, CatIfValue>;

export type CatIfDictValue = {
[key: string]: [
number,
{ [header: string]: string | [string, CommonAccessTokenDict] },
string?
];
};

export class CommonAccessTokenIf {
private catIfMap: CommonAccessTokenIfMap = new Map();

Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,9 @@ export class MethodNotAllowedError extends Error {
super(`HTTP Method ${method} not allowed`);
}
}

export class InvalidCatIfError extends Error {
constructor(reason: string) {
super(reason);
}
}
115 changes: 115 additions & 0 deletions src/validators/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,121 @@ describe('HTTP Request CAT Validator with auto renew', () => {
expect(response.getHeader('cta-common-access-token')).not.toBeDefined();
});

test('can follow the directives in catif claim when no acceptable token is provided', async () => {
const json = {
iss: 'eyevinn',
exp: Math.floor(Date.now() / 1000) - 60,
cti: 'foobar',
catif: {
exp: [
307,
{
Location: 'https://auth.example.net/'
}
]
}
};
base64encoded = await generator.generateFromJson(json, {
type: 'mac',
alg: 'HS256',
kid: 'Symmetric256',
generateCwtId: true
});
// Validate auto renew
const httpValidator = new HttpValidator({
keys: [
{
kid: 'Symmetric256',
key: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
}
],
issuer: 'eyevinn'
});
const request = createRequest({
method: 'GET',
headers: {
'CTA-Common-Access-Token': base64encoded
}
});
const response = createResponse();
const result = await httpValidator.validateHttpRequest(request, response);
expect(response.getHeader('Location')).toBe('https://auth.example.net/');
expect(result.status).toBe(307);
});

test('can generate new cat claims based on catif directives', async () => {
const json = {
iss: 'eyevinn',
exp: Math.floor(Date.now() / 1000) - 60,
cti: 'foobar',
catif: {
exp: [
307,
{
Location: [
'https://auth.example.net/?CAT=',
{
iss: null,
iat: null,
catu: {
host: { 'exact-match': 'auth.example.net' }
}
}
]
},
'Symmetric256'
]
}
};
base64encoded = await generator.generateFromJson(json, {
type: 'mac',
alg: 'HS256',
kid: 'Symmetric256',
generateCwtId: true
});
// Validate auto renew
const httpValidator = new HttpValidator({
keys: [
{
kid: 'Symmetric256',
key: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
}
],
issuer: 'eyevinn'
});
const request = createRequest({
method: 'GET',
headers: {
'CTA-Common-Access-Token': base64encoded
}
});
const response = createResponse();
const result = await httpValidator.validateHttpRequest(request, response);
expect(response.getHeader('Location')).toBeDefined();
const location = new URL(response.getHeader('Location') as string);
const newToken = location.searchParams.get('CAT');
expect(newToken).toBeDefined();
expect(result.status).toBe(307);
const validator = new CAT({
keys: {
Symmetric256: Buffer.from(
'403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388',
'hex'
)
}
});
const result2 = await validator.validate(newToken!, 'mac', {
issuer: 'eyevinn'
});
expect(result2.cat?.claims.iat).toBeDefined();
});

test.skip('can handle autorenew of type redirect', async () => {
// Validate auto renew
const httpValidator = new HttpValidator({
Expand Down
72 changes: 68 additions & 4 deletions src/validators/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { IncomingMessage, OutgoingMessage } from 'node:http';
import { CAT, CommonAccessToken } from '..';
import {
InvalidAudienceError,
InvalidCatIfError,
InvalidIssuerError,
InvalidReuseDetected,
KeyNotFoundError,
Expand All @@ -15,9 +16,10 @@ import {
CloudFrontRequest,
CloudFrontResponse
} from 'aws-lambda';
import { CommonAccessTokenDict } from '../cat';
import { CommonAccessTokenDict, CommonAccessTokenFactory } from '../cat';
import { ICTIStore } from '../stores/interface';
import { ITokenLogger } from '../loggers/interface';
import { CatIfDictValue } from '../catif';

interface HttpValidatorKey {
kid: string;
Expand Down Expand Up @@ -257,8 +259,19 @@ export class HttpValidator {
}
return { status: code, claims: cat?.claims, count };
} else {
// CAT valid but not acceptable
let responseCode = 401;
if (result.error instanceof TokenExpiredError) {
responseCode =
(cat?.claims.catif &&
(await this.handleCatIf({
cat,
response
}))) ||
401;
}
return {
status: 401,
status: responseCode,
message: result.error.message,
claims: cat?.claims
};
Expand All @@ -274,8 +287,9 @@ export class HttpValidator {
err instanceof InvalidReuseDetected ||
err instanceof MethodNotAllowedError
) {
const responseCode = 401;
return {
status: 401,
status: responseCode,
message: (err as Error).message,
claims: cat?.claims
};
Expand Down Expand Up @@ -322,6 +336,54 @@ export class HttpValidator {
}
}

private async handleCatIf({
cat,
response
}: {
cat: CommonAccessToken;
response?: OutgoingMessage;
}): Promise<number> {
if (!response) {
throw new Error('Missing response object in HTTP validator');
}
let returnCode = 401;
const catif: CatIfDictValue = cat.claims.catif as CatIfDictValue;
if (catif) {
for (const claim in catif) {
if (cat.claims[claim] !== undefined) {
const [code, value, keyid] = catif[claim];
returnCode = code;
for (const header in value) {
if (!Array.isArray(value[header])) {
response.setHeader(header, value[header] as string);
} else {
const newCatDict = value[header][1] as CommonAccessTokenDict;
if (!newCatDict['iss']) {
newCatDict['iss'] = this.opts.issuer;
}
if (!newCatDict['iat']) {
newCatDict['iat'] = Math.floor(Date.now() / 1000);
}
const newCat = CommonAccessTokenFactory.fromDict(newCatDict);
if (!keyid) {
throw new InvalidCatIfError('Missing key id in catif claim');
}
await newCat.mac(
{ kid: keyid, k: this.keys[keyid] },
this.opts.alg || 'HS256',
{ addCwtTag: true }
);
const newToken = newCat.raw?.toString('base64');
const newUrl = new URL(value[header][0] + newToken);
response.setHeader(header, newUrl.toString());
}
}
}
}
}
return returnCode;
}

private async handleReplay({
cat,
count
Expand Down Expand Up @@ -358,6 +420,7 @@ export class HttpValidator {
if (!cat.keyId) {
throw new Error('Key ID not found');
}
let responseCode = 200;
if (
cat &&
cat?.shouldRenew &&
Expand Down Expand Up @@ -402,9 +465,10 @@ export class HttpValidator {
redirectUrl.searchParams.delete(this.tokenUriParam);
redirectUrl.searchParams.set(this.tokenUriParam, renewedToken);
response.setHeader('Location', redirectUrl.toString());
responseCode = 302;
}
}
}
return { status: 200 };
return { status: responseCode };
}
}
Loading