Skip to content

Commit 83ed8d1

Browse files
authored
Merge pull request #423 from MatrixAI/feature-polykey-auth-login
Added `polykey auth login` command
2 parents 094c48f + be4d177 commit 83ed8d1

File tree

11 files changed

+347
-8
lines changed

11 files changed

+347
-8
lines changed

npmDepsHash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sha256-ckDhoiFXCuB/abIn/GSYKFHMIwe+7pTHCoTbc3Hsyuo=
1+
sha256-QZafJAHalDT2QQhw4evfJ1fun7saN4Mz08JX8YP4SDQ=

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
"mocked-env": "^1.3.5",
162162
"nexpect": "^0.6.0",
163163
"node-gyp-build": "^4.8.4",
164-
"polykey": "^2.3.5",
164+
"polykey": "^2.4.0",
165165
"shelljs": "^0.8.5",
166166
"shx": "^0.3.4",
167167
"tsx": "^3.12.7",

src/auth/CommandAuth.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import CommandLogin from './CommandLogin.js';
2+
import CommandPolykey from '../CommandPolykey.js';
3+
4+
class CommandAuth extends CommandPolykey {
5+
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
6+
super(...args);
7+
this.name('auth');
8+
this.description('Authentication operations');
9+
this.addCommand(new CommandLogin(...args));
10+
}
11+
}
12+
13+
export default CommandAuth;

src/auth/CommandLogin.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import type PolykeyClient from 'polykey/PolykeyClient.js';
2+
import type {
3+
TokenPayloadEncoded,
4+
TokenProtectedHeaderEncoded,
5+
TokenSignatureEncoded,
6+
} from 'polykey/tokens/types.js';
7+
import type { IdentityRequestData } from 'polykey/client/types.js';
8+
import CommandPolykey from '../CommandPolykey.js';
9+
import * as binProcessors from '../utils/processors.js';
10+
import * as binParsers from '../utils/parsers.js';
11+
import * as binUtils from '../utils/index.js';
12+
import * as binOptions from '../utils/options.js';
13+
import * as errors from '../errors.js';
14+
15+
class CommandLogin extends CommandPolykey {
16+
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
17+
super(...args);
18+
this.name('login');
19+
this.description('Login to a platform with Polykey identity');
20+
this.argument(
21+
'<token>',
22+
'Token provided by platform for logging in',
23+
binParsers.parseCompactJWT,
24+
);
25+
this.addOption(binOptions.nodeId);
26+
this.addOption(binOptions.clientHost);
27+
this.addOption(binOptions.clientPort);
28+
this.action(async (encodedToken, options) => {
29+
const { default: PolykeyClient } = await import(
30+
'polykey/PolykeyClient.js'
31+
);
32+
const tokensUtils = await import('polykey/tokens/utils.js');
33+
const clientOptions = await binProcessors.processClientOptions(
34+
options.nodePath,
35+
options.nodeId,
36+
options.clientHost,
37+
options.clientPort,
38+
this.fs,
39+
this.logger.getChild(binProcessors.processClientOptions.name),
40+
);
41+
const meta = await binProcessors.processAuthentication(
42+
options.passwordFile,
43+
this.fs,
44+
);
45+
46+
let pkClient: PolykeyClient;
47+
this.exitHandlers.handlers.push(async () => {
48+
if (pkClient != null) await pkClient.stop();
49+
});
50+
try {
51+
pkClient = await PolykeyClient.createPolykeyClient({
52+
nodeId: clientOptions.nodeId,
53+
host: clientOptions.clientHost,
54+
port: clientOptions.clientPort,
55+
options: {
56+
nodePath: options.nodePath,
57+
},
58+
logger: this.logger.getChild(PolykeyClient.name),
59+
});
60+
61+
// Create a JSON representation of the encoded header
62+
const [protectedHeader, payload, signature] = encodedToken;
63+
const incomingTokenEncoded = {
64+
payload: payload as TokenPayloadEncoded,
65+
signatures: [
66+
{
67+
protected: protectedHeader as TokenProtectedHeaderEncoded,
68+
signature: signature as TokenSignatureEncoded,
69+
},
70+
],
71+
};
72+
73+
// Get it verified and signed by the agent
74+
const response = await binUtils.retryAuthentication(
75+
(auth) =>
76+
pkClient.rpcClient.methods.authSignToken({
77+
metadata: auth,
78+
...incomingTokenEncoded,
79+
}),
80+
meta,
81+
);
82+
83+
// Send the returned JWT to the returnURL provided by the initial token
84+
const compactHeader = binUtils.jsonToCompactJWT(response);
85+
const incomingPayload =
86+
tokensUtils.parseTokenPayload<IdentityRequestData>(payload);
87+
let result: Response;
88+
try {
89+
result = await fetch(incomingPayload.returnURL, {
90+
method: 'POST',
91+
body: JSON.stringify({ token: compactHeader }),
92+
});
93+
} catch (e) {
94+
throw new errors.ErrorPolykeyCLILoginFailed(
95+
'Failed to send token to return url',
96+
{ cause: e },
97+
);
98+
}
99+
100+
// Handle non-200 response
101+
if (!result.ok) {
102+
throw new errors.ErrorPolykeyCLILoginFailed(
103+
`Return url returned failure with code ${result.status}`,
104+
);
105+
}
106+
} finally {
107+
if (pkClient! != null) await pkClient.stop();
108+
}
109+
});
110+
}
111+
}
112+
113+
export default CommandLogin;

src/auth/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './CommandAuth.js';

src/errors.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,16 @@ class ErrorPolykeyCLITouchSecret<T> extends ErrorPolykeyCLI<T> {
196196
exitCode = 1;
197197
}
198198

199+
class ErrorPolykeyCLIInvalidJWT<T> extends ErrorPolykeyCLI<T> {
200+
static description: 'JWT is not valid';
201+
exitCode = sysexits.USAGE;
202+
}
203+
204+
class ErrorPolykeyCLILoginFailed<T> extends ErrorPolykeyCLI<T> {
205+
static description = 'Failed to login using Polykey';
206+
exitCode = sysexits.SOFTWARE;
207+
}
208+
199209
export {
200210
ErrorPolykeyCLI,
201211
ErrorPolykeyCLIUncaughtException,
@@ -224,4 +234,6 @@ export {
224234
ErrorPolykeyCLICatSecret,
225235
ErrorPolykeyCLIEditSecret,
226236
ErrorPolykeyCLITouchSecret,
237+
ErrorPolykeyCLIInvalidJWT,
238+
ErrorPolykeyCLILoginFailed,
227239
};

src/polykey.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ async function polykeyMain(argv: Array<string>): Promise<number> {
152152
const { default: CommandBootstrap } = await import('./bootstrap/index.js');
153153
const { default: CommandAgent } = await import('./agent/index.js');
154154
const { default: CommandAudit } = await import('./audit/index.js');
155+
const { default: CommandAuth } = await import('./auth/index.js');
155156
const { default: CommandVaults } = await import('./vaults/index.js');
156157
const { default: CommandSecrets } = await import('./secrets/index.js');
157158
const { default: CommandKeys } = await import('./keys/index.js');
@@ -181,6 +182,7 @@ async function polykeyMain(argv: Array<string>): Promise<number> {
181182
rootCommand.addCommand(new CommandBootstrap({ exitHandlers, fs }));
182183
rootCommand.addCommand(new CommandAgent({ exitHandlers, fs }));
183184
rootCommand.addCommand(new CommandAudit({ exitHandlers, fs }));
185+
rootCommand.addCommand(new CommandAuth({ exitHandlers, fs }));
184186
rootCommand.addCommand(new CommandNodes({ exitHandlers, fs }));
185187
rootCommand.addCommand(new CommandSecrets({ exitHandlers, fs }));
186188
rootCommand.addCommand(new CommandKeys({ exitHandlers, fs }));

src/utils/parsers.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const vaultNameRegex = /^(?!.*[:])[ -~\t\n]*$/s;
1313
const secretPathRegex = /^(?!.*[=])[ -~\t\n]*$/s;
1414
const secretPathValueRegex = /^([a-zA-Z_][\w]+)?$/;
1515
const environmentVariableRegex = /^([a-zA-Z_]+[a-zA-Z0-9_]*)?$/;
16+
const base64UrlRegex = /^[A-Za-z0-9\-_]+$/;
1617

1718
/**
1819
* Converts a validation parser to commander argument parser
@@ -192,6 +193,30 @@ const parsePort: (data: string) => Port = validateParserToArgParser(
192193
const parseSeedNodes: (data: string) => [SeedNodes, boolean] =
193194
validateParserToArgParser(nodesUtils.parseSeedNodes);
194195

196+
// Compact JWTs are in xxxx.yyyy.zzzz format where x is the protected
197+
// header, y is the payload, and z is the binary signature.
198+
const parseCompactJWT = (token: string): [string, string, string] => {
199+
// Clean up whitespaces
200+
token = token.trim();
201+
202+
// Confirm part amount
203+
const parts = token.split('.');
204+
if (parts.length !== 3) {
205+
throw new InvalidArgumentError(
206+
'JWT must contain three dot-separated parts',
207+
);
208+
}
209+
210+
// Validate base64 encoding
211+
for (const part of parts) {
212+
if (!part || !base64UrlRegex.test(part)) {
213+
throw new InvalidArgumentError('JWT is not correctly encoded');
214+
}
215+
}
216+
217+
return [parts[0], parts[1], parts[2]];
218+
};
219+
195220
export {
196221
vaultNameRegex,
197222
secretPathRegex,
@@ -220,4 +245,5 @@ export {
220245
parseProviderId,
221246
parseIdentityId,
222247
parseProviderIdList,
248+
parseCompactJWT,
223249
};

src/utils/utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { FileSystem } from 'polykey/types.js';
2-
import type { POJO } from 'polykey/types.js';
1+
import type { FileSystem, POJO } from 'polykey/types.js';
2+
import type { SignedTokenEncoded } from 'polykey/tokens/types.js';
33
import type {
44
TableRow,
55
TableOptions,
@@ -637,6 +637,15 @@ async function importFS(fs?: FileSystem): Promise<FileSystem> {
637637
return fsImported;
638638
}
639639

640+
function jsonToCompactJWT(token: SignedTokenEncoded): string {
641+
if (token.signatures.length !== 1) {
642+
throw new errors.ErrorPolykeyCLIInvalidJWT(
643+
'Too many signatures, expected 1',
644+
);
645+
}
646+
return `${token.signatures[0].protected}.${token.payload}.${token.signatures[0].signature}`;
647+
}
648+
640649
export {
641650
verboseToLogLevel,
642651
standardErrorReplacer,
@@ -660,6 +669,7 @@ export {
660669
generateVersionString,
661670
promise,
662671
importFS,
672+
jsonToCompactJWT,
663673
};
664674

665675
export type { OutputObject };

0 commit comments

Comments
 (0)