diff --git a/src/client/handlers/AuthIdentityToken.ts b/src/client/handlers/AuthIdentityToken.ts index 9c81a20fc..ff11b7e75 100644 --- a/src/client/handlers/AuthIdentityToken.ts +++ b/src/client/handlers/AuthIdentityToken.ts @@ -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'; @@ -25,7 +25,7 @@ class AuthIdentityToken extends UnaryHandler< if (jti == null) { throw new clientErrors.ErrorClientAuthenticationInvalidJTI(); } - const outgoingToken = Token.fromPayload({ + const outgoingToken = Token.fromPayload({ jti: jti.toMultibase('base64'), exp: Math.floor(Date.now() / 1000) + 60, // 60 seconds after issuing iss: nodesUtils.encodeNodeId(keyRing.getNodeId()), diff --git a/src/client/types.ts b/src/client/types.ts index 5b6f30837..f81b3dea1 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -108,12 +108,6 @@ type TokenMessage = { token: ProviderToken; }; -type IdentityResponseData = TokenPayload & { - jti: string; - exp: number; - iss: NodeIdEncoded; -}; - type TokenIdentityResponse = SignedTokenEncoded; // Nodes messages @@ -421,7 +415,6 @@ export type { ClaimIdMessage, ClaimNodeMessage, TokenMessage, - IdentityResponseData, TokenIdentityResponse, NodeIdMessage, AddressMessage, diff --git a/src/tokens/Token.ts b/src/tokens/Token.ts index f11ad1e1d..ba4c67358 100644 --- a/src/tokens/Token.ts +++ b/src/tokens/Token.ts @@ -79,6 +79,23 @@ class Token

{ ); } + public static fromCompact

( + signedTokenEncoded: string, + ): Token

{ + 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, @@ -257,6 +274,20 @@ class Token

{ 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; diff --git a/src/tokens/payloads/authIdentityToken.ts b/src/tokens/payloads/authIdentityToken.ts new file mode 100644 index 000000000..1f1a2761b --- /dev/null +++ b/src/tokens/payloads/authIdentityToken.ts @@ -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 { + const encodedToken = + tokensUtils.parseSignedToken(authIdentityEncoded); + const authIdentity = + tokensUtils.parseTokenPayload(encodedToken); + assertAuthSignedIdentity(authIdentity); + return encodedToken; +} + +export { assertAuthSignedIdentity, parseAuthSignedIdentity }; + +export type { AuthIdentityToken }; diff --git a/src/tokens/payloads/index.ts b/src/tokens/payloads/index.ts new file mode 100644 index 000000000..165a13c55 --- /dev/null +++ b/src/tokens/payloads/index.ts @@ -0,0 +1 @@ +export * from './authIdentityToken.js'; \ No newline at end of file diff --git a/src/tokens/types.ts b/src/tokens/types.ts index 75452a3b8..b6d91b8c4 100644 --- a/src/tokens/types.ts +++ b/src/tokens/types.ts @@ -118,6 +118,8 @@ type SignedTokenEncoded = { signatures: Array; }; +type CompactToken = Opaque<'CompactToken', string>; + export type { TokenPayload, TokenPayloadEncoded, @@ -132,4 +134,5 @@ export type { SignedToken, SignedTokenJSON, SignedTokenEncoded, + CompactToken, }; diff --git a/src/tokens/utils.ts b/src/tokens/utils.ts index 30308dd73..77a5d8c56 100644 --- a/src/tokens/utils.ts +++ b/src/tokens/utils.ts @@ -9,6 +9,7 @@ import type { SignedToken, SignedTokenEncoded, TokenHeaderSignatureEncoded, + CompactToken, } from './types.js'; import { Buffer } from 'buffer'; import canonicalize from 'canonicalize'; @@ -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)!; @@ -250,6 +254,22 @@ function parseSignedToken

( }; } +/** + * 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, @@ -261,4 +281,5 @@ export { parseTokenSignature, parseTokenHeaderSignature, parseSignedToken, + assertCompactToken, };