Skip to content

Commit cba7ab2

Browse files
authored
Merge pull request #912 from MatrixAI/feature-align-with-tailscale-login
Aligned rpc with tailscale login flow
2 parents 17638a1 + d40fbc9 commit cba7ab2

File tree

10 files changed

+81
-145
lines changed

10 files changed

+81
-145
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { HandlerTypes } from '@matrixai/rpc';
2+
import type AuthIdentityToken from '../handlers/AuthIdentityToken.js';
3+
import { UnaryCaller } from '@matrixai/rpc';
4+
5+
type CallerTypes = HandlerTypes<AuthIdentityToken>;
6+
7+
const authIdentityToken = new UnaryCaller<
8+
CallerTypes['input'],
9+
CallerTypes['output']
10+
>();
11+
12+
export default authIdentityToken;

src/client/callers/authSignToken.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/client/callers/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import agentStop from './agentStop.js';
44
import agentUnlock from './agentUnlock.js';
55
import auditEventsGet from './auditEventsGet.js';
66
import auditMetricGet from './auditMetricGet.js';
7-
import authSignToken from './authSignToken.js';
7+
import authIdentityToken from './authIdentityToken.js';
88
import gestaltsActionsGetByIdentity from './gestaltsActionsGetByIdentity.js';
99
import gestaltsActionsGetByNode from './gestaltsActionsGetByNode.js';
1010
import gestaltsActionsSetByIdentity from './gestaltsActionsSetByIdentity.js';
@@ -86,7 +86,7 @@ const clientManifest = {
8686
agentUnlock,
8787
auditEventsGet,
8888
auditMetricGet,
89-
authSignToken,
89+
authIdentityToken,
9090
gestaltsActionsGetByIdentity,
9191
gestaltsActionsGetByNode,
9292
gestaltsActionsSetByIdentity,
@@ -167,7 +167,7 @@ export {
167167
agentStop,
168168
agentUnlock,
169169
auditEventsGet,
170-
authSignToken,
170+
authIdentityToken,
171171
gestaltsActionsGetByIdentity,
172172
gestaltsActionsGetByNode,
173173
gestaltsActionsSetByIdentity,

src/client/errors.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ class ErrorClientProtocolError<T> extends ErrorClient<T> {
2828
exitCode = sysexits.USAGE;
2929
}
3030

31+
class ErrorClientAuthenticationInvalidJTI<T> extends ErrorClient<T> {
32+
static description = 'Failed to generate JTI';
33+
exitCode = sysexits.PROTOCOL;
34+
}
35+
3136
class ErrorClientService<T> extends ErrorClient<T> {}
3237

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

53-
class ErrorClientAuthenticationInvalidToken<T> extends ErrorClient<T> {
54-
static description = 'Token is invalid';
55-
exitCode = sysexits.PROTOCOL;
56-
}
57-
5858
export {
5959
ErrorClient,
6060
ErrorClientAuthMissing,
6161
ErrorClientAuthFormat,
6262
ErrorClientAuthDenied,
6363
ErrorClientInvalidHeader,
6464
ErrorClientProtocolError,
65+
ErrorClientAuthenticationInvalidJTI,
6566
ErrorClientService,
6667
ErrorClientServiceRunning,
6768
ErrorClientServiceNotRunning,
6869
ErrorClientServiceDestroyed,
6970
ErrorClientVerificationFailed,
70-
ErrorClientAuthenticationInvalidToken,
7171
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type {
2+
ClientRPCRequestParams,
3+
ClientRPCResponseResult,
4+
IdentityResponseData,
5+
TokenIdentityResponse,
6+
} from '../types.js';
7+
import type KeyRing from '../../keys/KeyRing.js';
8+
import { IdSortable } from '@matrixai/id';
9+
import { UnaryHandler } from '@matrixai/rpc';
10+
import Token from '../../tokens/Token.js';
11+
import * as nodesUtils from '../../nodes/utils.js';
12+
import * as clientErrors from '../errors.js';
13+
14+
class AuthIdentityToken extends UnaryHandler<
15+
{
16+
keyRing: KeyRing;
17+
},
18+
ClientRPCRequestParams,
19+
ClientRPCResponseResult<TokenIdentityResponse>
20+
> {
21+
public handle = async (): Promise<TokenIdentityResponse> => {
22+
const { keyRing }: { keyRing: KeyRing } = this.container;
23+
const idGen = new IdSortable();
24+
const jti = idGen.next().value;
25+
if (jti == null) {
26+
throw new clientErrors.ErrorClientAuthenticationInvalidJTI();
27+
}
28+
const outgoingToken = Token.fromPayload<IdentityResponseData>({
29+
jti: jti.toMultibase('base64'),
30+
exp: Math.floor(Date.now() / 1000) + 60, // 60 seconds after issuing
31+
iss: nodesUtils.encodeNodeId(keyRing.getNodeId()),
32+
});
33+
outgoingToken.signWithPrivateKey(keyRing.keyPair);
34+
return outgoingToken.toEncoded();
35+
};
36+
}
37+
38+
export default AuthIdentityToken;

src/client/handlers/AuthSignToken.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

src/client/handlers/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import AgentStop from './AgentStop.js';
2222
import AgentUnlock from './AgentUnlock.js';
2323
import AuditEventsGet from './AuditEventsGet.js';
2424
import AuditMetricGet from './AuditMetricGet.js';
25-
import AuthSignToken from './AuthSignToken.js';
25+
import AuthIdentityToken from './AuthIdentityToken.js';
2626
import GestaltsActionsGetByIdentity from './GestaltsActionsGetByIdentity.js';
2727
import GestaltsActionsGetByNode from './GestaltsActionsGetByNode.js';
2828
import GestaltsActionsSetByIdentity from './GestaltsActionsSetByIdentity.js';
@@ -123,7 +123,7 @@ const serverManifest = (container: {
123123
agentUnlock: new AgentUnlock(container),
124124
auditEventsGet: new AuditEventsGet(container),
125125
auditMetricGet: new AuditMetricGet(container),
126-
authSignToken: new AuthSignToken(container),
126+
authIdentityToken: new AuthIdentityToken(container),
127127
gestaltsActionsGetByIdentity: new GestaltsActionsGetByIdentity(container),
128128
gestaltsActionsGetByNode: new GestaltsActionsGetByNode(container),
129129
gestaltsActionsSetByIdentity: new GestaltsActionsSetByIdentity(container),
@@ -210,7 +210,7 @@ export {
210210
AgentUnlock,
211211
AuditEventsGet,
212212
AuditMetricGet,
213-
AuthSignToken,
213+
AuthIdentityToken,
214214
GestaltsActionsGetByIdentity,
215215
GestaltsActionsGetByNode,
216216
GestaltsActionsSetByIdentity,

src/client/types.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,10 @@ type TokenMessage = {
108108
token: ProviderToken;
109109
};
110110

111-
// Return URL must be present on the token, otherwise token contents is decided
112-
// by the client.
113-
type IdentityRequestData = TokenPayload & {
114-
returnURL: string;
115-
publicKey: string;
116-
};
117-
118-
type TokenIdentityRequest = SignedTokenEncoded;
119-
120111
type IdentityResponseData = TokenPayload & {
121-
requestToken: TokenIdentityRequest;
122-
nodeId: NodeIdEncoded;
112+
jti: string;
113+
exp: number;
114+
iss: NodeIdEncoded;
123115
};
124116

125117
type TokenIdentityResponse = SignedTokenEncoded;
@@ -422,9 +414,7 @@ export type {
422414
ClaimIdMessage,
423415
ClaimNodeMessage,
424416
TokenMessage,
425-
IdentityRequestData,
426417
IdentityResponseData,
427-
TokenIdentityRequest,
428418
TokenIdentityResponse,
429419
NodeIdMessage,
430420
AddressMessage,

tests/client/handlers/auth.test.ts

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import type {
2-
IdentityRequestData,
3-
IdentityResponseData,
4-
} from '#src/client/types.js';
1+
import type { IdentityResponseData } from '#client/types.js';
52
import type { TLSConfig } from '#network/types.js';
63
import fs from 'node:fs';
74
import path from 'node:path';
@@ -10,17 +7,17 @@ import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger';
107
import { RPCClient } from '@matrixai/rpc';
118
import { WebSocketClient } from '@matrixai/ws';
129
import * as testsUtils from '../../utils/index.js';
13-
import { AuthSignToken } from '#client/handlers/index.js';
14-
import { authSignToken } from '#client/callers/index.js';
10+
import { AuthIdentityToken } from '#client/handlers/index.js';
11+
import { authIdentityToken } from '#client/callers/index.js';
1512
import KeyRing from '#keys/KeyRing.js';
1613
import Token from '#tokens/Token.js';
1714
import ClientService from '#client/ClientService.js';
1815
import * as keysUtils from '#keys/utils/index.js';
1916
import * as networkUtils from '#network/utils.js';
20-
import * as clientErrors from '#client/errors.js';
17+
import * as nodesUtils from '#nodes/utils.js';
2118

22-
describe('authSignToken', () => {
23-
const logger = new Logger('authSignToken test', LogLevel.WARN, [
19+
describe('authIdentityToken', () => {
20+
const logger = new Logger('authIdentityToken test', LogLevel.WARN, [
2421
new StreamHandler(
2522
formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`,
2623
),
@@ -33,7 +30,7 @@ describe('authSignToken', () => {
3330
let clientService: ClientService;
3431
let webSocketClient: WebSocketClient;
3532
let rpcClient: RPCClient<{
36-
authSignToken: typeof authSignToken;
33+
authIdentityToken: typeof authIdentityToken;
3734
}>;
3835

3936
beforeEach(async () => {
@@ -56,7 +53,7 @@ describe('authSignToken', () => {
5653
});
5754
await clientService.start({
5855
manifest: {
59-
authSignToken: new AuthSignToken({
56+
authIdentityToken: new AuthIdentityToken({
6057
keyRing,
6158
}),
6259
},
@@ -72,7 +69,7 @@ describe('authSignToken', () => {
7269
});
7370
rpcClient = new RPCClient({
7471
manifest: {
75-
authSignToken,
72+
authIdentityToken,
7673
},
7774
streamFactory: () => webSocketClient.connection.newStream(),
7875
toError: networkUtils.toError,
@@ -91,44 +88,14 @@ describe('authSignToken', () => {
9188
});
9289
});
9390

94-
test('should sign a valid token', async () => {
95-
// Create token with separate key pair
96-
const keyPair = keysUtils.generateKeyPair();
97-
const token = Token.fromPayload<IdentityRequestData>({
98-
publicKey: keyPair.publicKey.toString('base64url'),
99-
returnURL: 'test',
100-
});
101-
token.signWithPrivateKey(keyPair);
102-
103-
// Get the node to sign the token as well
104-
const encodedToken = token.toEncoded();
105-
const identityToken = await rpcClient.methods.authSignToken(encodedToken);
106-
107-
// Check the signature of both the incoming token and the original sent token
91+
test('should return a signed token', async () => {
92+
const identityToken = await rpcClient.methods.authIdentityToken({});
10893
const decodedToken = Token.fromEncoded<IdentityResponseData>(identityToken);
10994
const decodedPublicKey = keysUtils.publicKeyFromNodeId(keyRing.getNodeId());
11095
expect(decodedToken.verifyWithPublicKey(decodedPublicKey)).toBeTrue();
111-
const requestToken = Token.fromEncoded<IdentityRequestData>(
112-
decodedToken.payload.requestToken,
113-
);
114-
expect(requestToken.verifyWithPublicKey(keyPair.publicKey)).toBeTrue();
115-
});
116-
117-
test('should fail if public key does not match signature', async () => {
118-
// Create token with a key pair and sign it with another
119-
const keyPair1 = keysUtils.generateKeyPair();
120-
const keyPair2 = keysUtils.generateKeyPair();
121-
const token = Token.fromPayload<IdentityRequestData>({
122-
publicKey: keyPair1.publicKey.toString('base64url'),
123-
returnURL: 'test',
124-
});
125-
token.signWithPrivateKey(keyPair2);
126-
127-
// The token should fail validation
128-
const encodedToken = token.toEncoded();
129-
await testsUtils.expectRemoteError(
130-
rpcClient.methods.authSignToken(encodedToken),
131-
clientErrors.ErrorClientAuthenticationInvalidToken,
132-
);
96+
const encodedNodeId = nodesUtils.encodeNodeId(keyRing.getNodeId());
97+
expect(decodedToken.payload.iss).toBe(encodedNodeId);
98+
expect(decodedToken.payload.exp).toBeDefined();
99+
expect(decodedToken.payload.jti).toBeDefined();
133100
});
134101
});

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"allowSyntheticDefaultImports": true,
1414
"resolveJsonModule": true,
1515
"moduleResolution": "NodeNext",
16-
"module": "ESNext",
16+
"module": "NodeNext",
1717
"target": "ES2022",
1818
"baseUrl": "./src",
1919
"paths": {

0 commit comments

Comments
 (0)