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
12 changes: 12 additions & 0 deletions src/client/callers/authIdentityToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HandlerTypes } from '@matrixai/rpc';
import type AuthIdentityToken from '../handlers/AuthIdentityToken.js';
import { UnaryCaller } from '@matrixai/rpc';

type CallerTypes = HandlerTypes<AuthIdentityToken>;

const authIdentityToken = new UnaryCaller<
CallerTypes['input'],
CallerTypes['output']
>();

export default authIdentityToken;
12 changes: 0 additions & 12 deletions src/client/callers/authSignToken.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/client/callers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import agentStop from './agentStop.js';
import agentUnlock from './agentUnlock.js';
import auditEventsGet from './auditEventsGet.js';
import auditMetricGet from './auditMetricGet.js';
import authSignToken from './authSignToken.js';
import authIdentityToken from './authIdentityToken.js';
import gestaltsActionsGetByIdentity from './gestaltsActionsGetByIdentity.js';
import gestaltsActionsGetByNode from './gestaltsActionsGetByNode.js';
import gestaltsActionsSetByIdentity from './gestaltsActionsSetByIdentity.js';
Expand Down Expand Up @@ -86,7 +86,7 @@ const clientManifest = {
agentUnlock,
auditEventsGet,
auditMetricGet,
authSignToken,
authIdentityToken,
gestaltsActionsGetByIdentity,
gestaltsActionsGetByNode,
gestaltsActionsSetByIdentity,
Expand Down Expand Up @@ -167,7 +167,7 @@ export {
agentStop,
agentUnlock,
auditEventsGet,
authSignToken,
authIdentityToken,
gestaltsActionsGetByIdentity,
gestaltsActionsGetByNode,
gestaltsActionsSetByIdentity,
Expand Down
12 changes: 6 additions & 6 deletions src/client/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class ErrorClientProtocolError<T> extends ErrorClient<T> {
exitCode = sysexits.USAGE;
}

class ErrorClientAuthenticationInvalidJTI<T> extends ErrorClient<T> {
static description = 'Failed to generate JTI';
exitCode = sysexits.PROTOCOL;
}

class ErrorClientService<T> extends ErrorClient<T> {}

class ErrorClientServiceRunning<T> extends ErrorClientService<T> {
Expand All @@ -50,22 +55,17 @@ class ErrorClientVerificationFailed<T> extends ErrorClientService<T> {
exitCode = sysexits.USAGE;
}

class ErrorClientAuthenticationInvalidToken<T> extends ErrorClient<T> {
static description = 'Token is invalid';
exitCode = sysexits.PROTOCOL;
}

export {
ErrorClient,
ErrorClientAuthMissing,
ErrorClientAuthFormat,
ErrorClientAuthDenied,
ErrorClientInvalidHeader,
ErrorClientProtocolError,
ErrorClientAuthenticationInvalidJTI,
ErrorClientService,
ErrorClientServiceRunning,
ErrorClientServiceNotRunning,
ErrorClientServiceDestroyed,
ErrorClientVerificationFailed,
ErrorClientAuthenticationInvalidToken,
};
38 changes: 38 additions & 0 deletions src/client/handlers/AuthIdentityToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {
ClientRPCRequestParams,
ClientRPCResponseResult,
IdentityResponseData,
TokenIdentityResponse,
} from '../types.js';
import type KeyRing from '../../keys/KeyRing.js';
import { IdSortable } from '@matrixai/id';
import { UnaryHandler } from '@matrixai/rpc';
import Token from '../../tokens/Token.js';
import * as nodesUtils from '../../nodes/utils.js';
import * as clientErrors from '../errors.js';

class AuthIdentityToken extends UnaryHandler<
{
keyRing: KeyRing;
},
ClientRPCRequestParams,
ClientRPCResponseResult<TokenIdentityResponse>
> {
public handle = async (): Promise<TokenIdentityResponse> => {
const { keyRing }: { keyRing: KeyRing } = this.container;
const idGen = new IdSortable();
const jti = idGen.next().value;
if (jti == null) {
throw new clientErrors.ErrorClientAuthenticationInvalidJTI();
}
const outgoingToken = Token.fromPayload<IdentityResponseData>({
jti: jti.toMultibase('base64'),
exp: Math.floor(Date.now() / 1000) + 60, // 60 seconds after issuing
iss: nodesUtils.encodeNodeId(keyRing.getNodeId()),
});
outgoingToken.signWithPrivateKey(keyRing.keyPair);
return outgoingToken.toEncoded();
};
}

export default AuthIdentityToken;
59 changes: 0 additions & 59 deletions src/client/handlers/AuthSignToken.ts

This file was deleted.

6 changes: 3 additions & 3 deletions src/client/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import AgentStop from './AgentStop.js';
import AgentUnlock from './AgentUnlock.js';
import AuditEventsGet from './AuditEventsGet.js';
import AuditMetricGet from './AuditMetricGet.js';
import AuthSignToken from './AuthSignToken.js';
import AuthIdentityToken from './AuthIdentityToken.js';
import GestaltsActionsGetByIdentity from './GestaltsActionsGetByIdentity.js';
import GestaltsActionsGetByNode from './GestaltsActionsGetByNode.js';
import GestaltsActionsSetByIdentity from './GestaltsActionsSetByIdentity.js';
Expand Down Expand Up @@ -123,7 +123,7 @@ const serverManifest = (container: {
agentUnlock: new AgentUnlock(container),
auditEventsGet: new AuditEventsGet(container),
auditMetricGet: new AuditMetricGet(container),
authSignToken: new AuthSignToken(container),
authIdentityToken: new AuthIdentityToken(container),
gestaltsActionsGetByIdentity: new GestaltsActionsGetByIdentity(container),
gestaltsActionsGetByNode: new GestaltsActionsGetByNode(container),
gestaltsActionsSetByIdentity: new GestaltsActionsSetByIdentity(container),
Expand Down Expand Up @@ -210,7 +210,7 @@ export {
AgentUnlock,
AuditEventsGet,
AuditMetricGet,
AuthSignToken,
AuthIdentityToken,
GestaltsActionsGetByIdentity,
GestaltsActionsGetByNode,
GestaltsActionsSetByIdentity,
Expand Down
16 changes: 3 additions & 13 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,10 @@ type TokenMessage = {
token: ProviderToken;
};

// Return URL must be present on the token, otherwise token contents is decided
// by the client.
type IdentityRequestData = TokenPayload & {
returnURL: string;
publicKey: string;
};

type TokenIdentityRequest = SignedTokenEncoded;

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

type TokenIdentityResponse = SignedTokenEncoded;
Expand Down Expand Up @@ -422,9 +414,7 @@ export type {
ClaimIdMessage,
ClaimNodeMessage,
TokenMessage,
IdentityRequestData,
IdentityResponseData,
TokenIdentityRequest,
TokenIdentityResponse,
NodeIdMessage,
AddressMessage,
Expand Down
63 changes: 15 additions & 48 deletions tests/client/handlers/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import type {
IdentityRequestData,
IdentityResponseData,
} from '#src/client/types.js';
import type { IdentityResponseData } from '#client/types.js';
import type { TLSConfig } from '#network/types.js';
import fs from 'node:fs';
import path from 'node:path';
Expand All @@ -10,17 +7,17 @@ import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger';
import { RPCClient } from '@matrixai/rpc';
import { WebSocketClient } from '@matrixai/ws';
import * as testsUtils from '../../utils/index.js';
import { AuthSignToken } from '#client/handlers/index.js';
import { authSignToken } from '#client/callers/index.js';
import { AuthIdentityToken } from '#client/handlers/index.js';
import { authIdentityToken } from '#client/callers/index.js';
import KeyRing from '#keys/KeyRing.js';
import Token from '#tokens/Token.js';
import ClientService from '#client/ClientService.js';
import * as keysUtils from '#keys/utils/index.js';
import * as networkUtils from '#network/utils.js';
import * as clientErrors from '#client/errors.js';
import * as nodesUtils from '#nodes/utils.js';

describe('authSignToken', () => {
const logger = new Logger('authSignToken test', LogLevel.WARN, [
describe('authIdentityToken', () => {
const logger = new Logger('authIdentityToken test', LogLevel.WARN, [
new StreamHandler(
formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`,
),
Expand All @@ -33,7 +30,7 @@ describe('authSignToken', () => {
let clientService: ClientService;
let webSocketClient: WebSocketClient;
let rpcClient: RPCClient<{
authSignToken: typeof authSignToken;
authIdentityToken: typeof authIdentityToken;
}>;

beforeEach(async () => {
Expand All @@ -56,7 +53,7 @@ describe('authSignToken', () => {
});
await clientService.start({
manifest: {
authSignToken: new AuthSignToken({
authIdentityToken: new AuthIdentityToken({
keyRing,
}),
},
Expand All @@ -72,7 +69,7 @@ describe('authSignToken', () => {
});
rpcClient = new RPCClient({
manifest: {
authSignToken,
authIdentityToken,
},
streamFactory: () => webSocketClient.connection.newStream(),
toError: networkUtils.toError,
Expand All @@ -91,44 +88,14 @@ describe('authSignToken', () => {
});
});

test('should sign a valid token', async () => {
// Create token with separate key pair
const keyPair = keysUtils.generateKeyPair();
const token = Token.fromPayload<IdentityRequestData>({
publicKey: keyPair.publicKey.toString('base64url'),
returnURL: 'test',
});
token.signWithPrivateKey(keyPair);

// Get the node to sign the token as well
const encodedToken = token.toEncoded();
const identityToken = await rpcClient.methods.authSignToken(encodedToken);

// Check the signature of both the incoming token and the original sent token
test('should return a signed token', async () => {
const identityToken = await rpcClient.methods.authIdentityToken({});
const decodedToken = Token.fromEncoded<IdentityResponseData>(identityToken);
const decodedPublicKey = keysUtils.publicKeyFromNodeId(keyRing.getNodeId());
expect(decodedToken.verifyWithPublicKey(decodedPublicKey)).toBeTrue();
const requestToken = Token.fromEncoded<IdentityRequestData>(
decodedToken.payload.requestToken,
);
expect(requestToken.verifyWithPublicKey(keyPair.publicKey)).toBeTrue();
});

test('should fail if public key does not match signature', async () => {
// Create token with a key pair and sign it with another
const keyPair1 = keysUtils.generateKeyPair();
const keyPair2 = keysUtils.generateKeyPair();
const token = Token.fromPayload<IdentityRequestData>({
publicKey: keyPair1.publicKey.toString('base64url'),
returnURL: 'test',
});
token.signWithPrivateKey(keyPair2);

// The token should fail validation
const encodedToken = token.toEncoded();
await testsUtils.expectRemoteError(
rpcClient.methods.authSignToken(encodedToken),
clientErrors.ErrorClientAuthenticationInvalidToken,
);
const encodedNodeId = nodesUtils.encodeNodeId(keyRing.getNodeId());
expect(decodedToken.payload.iss).toBe(encodedNodeId);
expect(decodedToken.payload.exp).toBeDefined();
expect(decodedToken.payload.jti).toBeDefined();
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"moduleResolution": "NodeNext",
"module": "ESNext",
"module": "NodeNext",
"target": "ES2022",
"baseUrl": "./src",
"paths": {
Expand Down