Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/client/handlers/AuthIdentityToken.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type {
ClientRPCRequestParams,
ClientRPCResponseResult,
IdentityResponseData,
TokenIdentityResponse,
} from '../types.js';
import type { AuthIdentityToken as AuthIdentityJWT } from '../../tokens/payloads/authIdentityToken.js';
import type KeyRing from '../../keys/KeyRing.js';
import { IdSortable } from '@matrixai/id';
import { UnaryHandler } from '@matrixai/rpc';
Expand All @@ -25,7 +25,7 @@ class AuthIdentityToken extends UnaryHandler<
if (jti == null) {
throw new clientErrors.ErrorClientAuthenticationInvalidJTI();
}
const outgoingToken = Token.fromPayload<IdentityResponseData>({
const outgoingToken = Token.fromPayload<AuthIdentityJWT>({
jti: jti.toMultibase('base64'),
exp: Math.floor(Date.now() / 1000) + 60, // 60 seconds after issuing
iss: nodesUtils.encodeNodeId(keyRing.getNodeId()),
Expand Down
7 changes: 0 additions & 7 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,6 @@ type TokenMessage = {
token: ProviderToken;
};

type IdentityResponseData = TokenPayload & {
jti: string;
exp: number;
iss: NodeIdEncoded;
};

type TokenIdentityResponse = SignedTokenEncoded;

// Nodes messages
Expand Down Expand Up @@ -421,7 +415,6 @@ export type {
ClaimIdMessage,
ClaimNodeMessage,
TokenMessage,
IdentityResponseData,
TokenIdentityResponse,
NodeIdMessage,
AddressMessage,
Expand Down
31 changes: 31 additions & 0 deletions src/tokens/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@ class Token<P extends TokenPayload = TokenPayload> {
);
}

public static fromCompact<P extends TokenPayload = TokenPayload>(
signedTokenEncoded: string,
): Token<P> {
tokensUtils.assertCompactToken(signedTokenEncoded);
const [header, payload, signature] = signedTokenEncoded.split('.');
const tokenEncoded = {
payload: payload,
signatures: [
{
protected: header,
signature: signature,
},
],
} as SignedTokenEncoded;
return this.fromEncoded(tokenEncoded);
}

public constructor(
payload: P,
payloadEncoded: TokenPayloadEncoded,
Expand Down Expand Up @@ -257,6 +274,20 @@ class Token<P extends TokenPayload = TokenPayload> {
public toJSON() {
return this.toEncoded();
}

/**
* The compact, xxxx.yyyy.zzzz representation of this `Token` is `string`. The
* token must have exactly one signature, otherwise it cannot be converted to
* a compact token. This function will return undefined in that case.
*/
public toCompact(): string | undefined {
if (this.signatures.length !== 1) {
return;
}
const { payload, signatures } = this.toEncoded();
const { protected: header, signature } = signatures[0];
return `${header}.${payload}.${signature}`;
}
}

export default Token;
49 changes: 49 additions & 0 deletions src/tokens/payloads/authIdentityToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { SignedToken, TokenPayload } from '../types.js';
import type { NodeIdEncoded } from '../../ids/types.js';
import * as tokensUtils from '../utils.js';
import * as ids from '../../ids/index.js';
import * as validationErrors from '../../validation/errors.js';
import * as utils from '../../utils/index.js';

interface AuthIdentityToken extends TokenPayload {
iss: NodeIdEncoded;
exp: number;
jti: string;
}

function assertAuthSignedIdentity(
authIdentityToken: unknown,
): asserts authIdentityToken is AuthIdentityToken {
if (!utils.isObject(authIdentityToken)) {
throw new validationErrors.ErrorParse('must be POJO');
}
if (
authIdentityToken['iss'] == null ||
ids.decodeNodeId(authIdentityToken['iss'] == null)
) {
throw new validationErrors.ErrorParse(
'`iss` property must be an encoded node ID',
);
}
if (typeof authIdentityToken['exp'] !== 'number') {
throw new validationErrors.ErrorParse('`exp` property must be a number');
}
if (typeof authIdentityToken['jti'] !== 'string') {
throw new validationErrors.ErrorParse('`jti` property must be a string');
}
}

function parseAuthSignedIdentity(
authIdentityEncoded: unknown,
): SignedToken<AuthIdentityToken> {
const encodedToken =
tokensUtils.parseSignedToken<AuthIdentityToken>(authIdentityEncoded);
const authIdentity =
tokensUtils.parseTokenPayload<AuthIdentityToken>(encodedToken);
assertAuthSignedIdentity(authIdentity);
return encodedToken;
}

export { assertAuthSignedIdentity, parseAuthSignedIdentity };

export type { AuthIdentityToken };
1 change: 1 addition & 0 deletions src/tokens/payloads/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './authIdentityToken.js';
3 changes: 3 additions & 0 deletions src/tokens/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ type SignedTokenEncoded = {
signatures: Array<TokenHeaderSignatureEncoded>;
};

type CompactToken = Opaque<'CompactToken', string>;

export type {
TokenPayload,
TokenPayloadEncoded,
Expand All @@ -132,4 +134,5 @@ export type {
SignedToken,
SignedTokenJSON,
SignedTokenEncoded,
CompactToken,
};
21 changes: 21 additions & 0 deletions src/tokens/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
SignedToken,
SignedTokenEncoded,
TokenHeaderSignatureEncoded,
CompactToken,
} from './types.js';
import { Buffer } from 'buffer';
import canonicalize from 'canonicalize';
Expand All @@ -17,6 +18,9 @@ import * as validationErrors from '../validation/errors.js';
import * as keysUtils from '../keys/utils/index.js';
import * as utils from '../utils/index.js';

const compactTokenAssertRegex =
/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/;

function generateTokenPayload(payload: TokenPayload): TokenPayloadEncoded {
// @ts-ignore: canonicalize exports is function improperly for ESM
const payloadJSON = canonicalize(payload)!;
Expand Down Expand Up @@ -250,6 +254,22 @@ function parseSignedToken<P extends TokenPayload = TokenPayload>(
};
}

/**
* Asserts a value is a valid compact token
*/
function assertCompactToken(
compactToken: unknown,
): asserts compactToken is CompactToken {
if (typeof compactToken !== 'string') {
throw new validationErrors.ErrorParse('token must be a string');
}
if (!compactTokenAssertRegex.test(compactToken)) {
throw new validationErrors.ErrorParse(
'Input is not a compact JWT (format: xxxx.yyyy.zzzz, base64url-encoded)',
);
}
}

export {
generateTokenPayload,
generateTokenProtectedHeader,
Expand All @@ -261,4 +281,5 @@ export {
parseTokenSignature,
parseTokenHeaderSignature,
parseSignedToken,
assertCompactToken,
};
Loading