Skip to content

Commit b7b3bab

Browse files
committed
feat: consolidated token logic and added compact tokens
1 parent 161538e commit b7b3bab

File tree

6 files changed

+151
-0
lines changed

6 files changed

+151
-0
lines changed

src/tokens/Token.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,23 @@ class Token<P extends TokenPayload = TokenPayload> {
7979
);
8080
}
8181

82+
public static fromCompact<P extends TokenPayload = TokenPayload>(
83+
signedTokenEncoded: string,
84+
): Token<P> {
85+
tokensUtils.assertCompactToken(signedTokenEncoded);
86+
const [header, payload, signature] = signedTokenEncoded.split('.');
87+
const tokenEncoded = {
88+
payload: payload,
89+
signatures: [
90+
{
91+
protected: header,
92+
signature: signature,
93+
},
94+
],
95+
} as SignedTokenEncoded;
96+
return this.fromEncoded(tokenEncoded);
97+
}
98+
8299
public constructor(
83100
payload: P,
84101
payloadEncoded: TokenPayloadEncoded,
@@ -257,6 +274,20 @@ class Token<P extends TokenPayload = TokenPayload> {
257274
public toJSON() {
258275
return this.toEncoded();
259276
}
277+
278+
/**
279+
* The compact, xxxx.yyyy.zzzz representation of this `Token` is `string`. The
280+
* token must have exactly one signature, otherwise it cannot be converted to
281+
* a compact token. This function will return undefined in that case.
282+
*/
283+
public toCompact(): string | undefined {
284+
if (this.signatures.length !== 1) {
285+
return;
286+
}
287+
const { payload, signatures } = this.toEncoded();
288+
const { protected: header, signature } = signatures[0];
289+
return `${header}.${payload}.${signature}`;
290+
}
260291
}
261292

262293
export default Token;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { SignedToken, TokenPayload } from '../types.js';
2+
import type { NodeIdEncoded } from '../../ids/types.js';
3+
import * as tokensUtils from '../utils.js';
4+
import * as ids from '../../ids/index.js';
5+
import * as validationErrors from '../../validation/errors.js';
6+
import * as utils from '../../utils/index.js';
7+
8+
interface AuthSignedIdentity extends TokenPayload {
9+
typ: 'AuthSignedIdentity';
10+
iss: NodeIdEncoded;
11+
exp: number;
12+
jti: string;
13+
}
14+
15+
function assertAuthSignedIdentity(
16+
authSignedIdentity: unknown,
17+
): asserts authSignedIdentity is AuthSignedIdentity {
18+
if (!utils.isObject(authSignedIdentity)) {
19+
throw new validationErrors.ErrorParse('must be POJO');
20+
}
21+
if (authSignedIdentity['typ'] !== 'AuthSignedIdentity') {
22+
throw new validationErrors.ErrorParse(
23+
'`typ` property must be `AuthSignedToken`',
24+
);
25+
}
26+
if (
27+
authSignedIdentity['iss'] == null ||
28+
ids.decodeNodeId(authSignedIdentity['iss'] == null)
29+
) {
30+
throw new validationErrors.ErrorParse(
31+
'`iss` property must be an encoded node ID',
32+
);
33+
}
34+
if (typeof authSignedIdentity['exp'] !== 'number') {
35+
throw new validationErrors.ErrorParse('`exp` property must be a number');
36+
}
37+
if (typeof authSignedIdentity['jti'] !== 'string') {
38+
throw new validationErrors.ErrorParse('`jti` property must be a string');
39+
}
40+
}
41+
42+
function parseAuthSignedIdentity(
43+
authIdentityEncoded: unknown,
44+
): SignedToken<AuthSignedIdentity> {
45+
const encodedToken =
46+
tokensUtils.parseSignedToken<AuthSignedIdentity>(authIdentityEncoded);
47+
const authIdentity =
48+
tokensUtils.parseTokenPayload<AuthSignedIdentity>(encodedToken);
49+
assertAuthSignedIdentity(authIdentity);
50+
return encodedToken;
51+
}
52+
53+
export { assertAuthSignedIdentity, parseAuthSignedIdentity };
54+
55+
export type { AuthSignedIdentity };

src/tokens/payloads/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './authSignedIdentity.js';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { SignedToken, TokenPayload } from '../types.js';
2+
import * as tokensUtils from '../utils.js';
3+
import * as validationErrors from '../../validation/errors.js';
4+
import * as utils from '../../utils/index.js';
5+
6+
interface SessionToken extends TokenPayload {
7+
iat: number;
8+
}
9+
10+
function assertSessionToken(
11+
sessionToken: unknown,
12+
): asserts sessionToken is SessionToken {
13+
if (!utils.isObject(sessionToken)) {
14+
throw new validationErrors.ErrorParse('must be POJO');
15+
}
16+
if (sessionToken['iat'] !== 'number') {
17+
throw new validationErrors.ErrorParse('`iat` property must be a number');
18+
}
19+
}
20+
21+
function parseSessionToken(sessionToken: unknown): SignedToken<SessionToken> {
22+
if (typeof sessionToken !== 'string') {
23+
throw new validationErrors.ErrorParse('sessionToken must be a string');
24+
}
25+
let parsedToken: unknown;
26+
try {
27+
parsedToken = JSON.parse(sessionToken);
28+
} catch {
29+
throw new validationErrors.ErrorParse('sessionToken must be valid JSON');
30+
}
31+
const encodedToken = tokensUtils.parseSignedToken<SessionToken>(parsedToken);
32+
const sessionPayload =
33+
tokensUtils.parseTokenPayload<SessionToken>(encodedToken);
34+
assertSessionToken(sessionPayload);
35+
return encodedToken;
36+
}
37+
38+
export { assertSessionToken, parseSessionToken };
39+
40+
export type { SessionToken };

src/tokens/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ type SignedTokenEncoded = {
118118
signatures: Array<TokenHeaderSignatureEncoded>;
119119
};
120120

121+
type CompactToken = Opaque<'CompactToken', string>;
122+
121123
export type {
122124
TokenPayload,
123125
TokenPayloadEncoded,
@@ -132,4 +134,5 @@ export type {
132134
SignedToken,
133135
SignedTokenJSON,
134136
SignedTokenEncoded,
137+
CompactToken,
135138
};

src/tokens/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
SignedToken,
1010
SignedTokenEncoded,
1111
TokenHeaderSignatureEncoded,
12+
CompactToken,
1213
} from './types.js';
1314
import { Buffer } from 'buffer';
1415
import canonicalize from 'canonicalize';
@@ -17,6 +18,9 @@ import * as validationErrors from '../validation/errors.js';
1718
import * as keysUtils from '../keys/utils/index.js';
1819
import * as utils from '../utils/index.js';
1920

21+
const compactTokenAssertRegex =
22+
/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/;
23+
2024
function generateTokenPayload(payload: TokenPayload): TokenPayloadEncoded {
2125
// @ts-ignore: canonicalize exports is function improperly for ESM
2226
const payloadJSON = canonicalize(payload)!;
@@ -250,6 +254,22 @@ function parseSignedToken<P extends TokenPayload = TokenPayload>(
250254
};
251255
}
252256

257+
/**
258+
* Asserts a value is a valid compact token
259+
*/
260+
function assertCompactToken(
261+
compactToken: unknown,
262+
): asserts compactToken is CompactToken {
263+
if (typeof compactToken !== 'string') {
264+
throw new validationErrors.ErrorParse('token must be a string');
265+
}
266+
if (!compactTokenAssertRegex.test(compactToken)) {
267+
throw new validationErrors.ErrorParse(
268+
'Input is not a compact JWT (format: xxxx.yyyy.zzzz, base64url-encoded)',
269+
);
270+
}
271+
}
272+
253273
export {
254274
generateTokenPayload,
255275
generateTokenProtectedHeader,
@@ -261,4 +281,5 @@ export {
261281
parseTokenSignature,
262282
parseTokenHeaderSignature,
263283
parseSignedToken,
284+
assertCompactToken,
264285
};

0 commit comments

Comments
 (0)