Skip to content

Commit 2d5acff

Browse files
committed
fix: added proper parsing for incoming token
[ci skip]
1 parent 949ac9a commit 2d5acff

File tree

4 files changed

+69
-23
lines changed

4 files changed

+69
-23
lines changed

src/auth/CommandLogin.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,25 @@ import type {
77
import type { IdentityRequestData } from 'polykey/client/types.js';
88
import CommandPolykey from '../CommandPolykey.js';
99
import * as binProcessors from '../utils/processors.js';
10+
import * as binParsers from '../utils/parsers.js';
1011
import * as binUtils from '../utils/index.js';
1112
import * as binOptions from '../utils/options.js';
12-
import * as binErrors from '../errors.js';
13+
import * as errors from '../errors.js';
1314

1415
class CommandLogin extends CommandPolykey {
1516
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
1617
super(...args);
1718
this.name('login');
1819
this.description('Login to a platform with Polykey identity');
19-
this.argument('<token>', 'Token provided by platform for logging in');
20+
this.argument(
21+
'<token>',
22+
'Token provided by platform for logging in',
23+
binParsers.parseCompactJWT,
24+
);
2025
this.addOption(binOptions.nodeId);
2126
this.addOption(binOptions.clientHost);
2227
this.addOption(binOptions.clientPort);
23-
this.action(async (token, options) => {
28+
this.action(async (encodedToken, options) => {
2429
const { default: PolykeyClient } = await import(
2530
'polykey/PolykeyClient.js'
2631
);
@@ -52,10 +57,9 @@ class CommandLogin extends CommandPolykey {
5257
},
5358
logger: this.logger.getChild(PolykeyClient.name),
5459
});
55-
// Compact JWTs are in xxxx.yyyy.zzzz format where x is the protected
56-
// header, y is the payload, and z is the binary signature.
57-
const [protectedHeader, payload, signature]: [string, string, string] =
58-
token.split('.');
60+
61+
// Create a JSON representation of the encoded header
62+
const [protectedHeader, payload, signature] = encodedToken;
5963
const incomingTokenEncoded = {
6064
payload: payload as TokenPayloadEncoded,
6165
signatures: [
@@ -65,6 +69,8 @@ class CommandLogin extends CommandPolykey {
6569
},
6670
],
6771
};
72+
73+
// Get it verified and signed by the agent
6874
const response = await binUtils.retryAuthentication(
6975
(auth) =>
7076
pkClient.rpcClient.methods.authSignToken({
@@ -73,30 +79,28 @@ class CommandLogin extends CommandPolykey {
7379
}),
7480
meta,
7581
);
76-
// We don't expect multiple signatures so a compact JWT will suffice
77-
const compactHeader = `${response.signatures[0].protected}.${response.payload}.${response.signatures[0].signature}`;
78-
const incomingPayload = tokensUtils.parseTokenPayload<IdentityRequestData>(payload);
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);
7987
let result: Response;
8088
try {
81-
result = await fetch(incomingPayload.returnUrl, {
89+
result = await fetch(incomingPayload.returnURL, {
8290
method: 'POST',
8391
body: JSON.stringify({ token: compactHeader }),
8492
});
8593
} catch (e) {
86-
throw new binErrors.ErrorPolykeyCLILoginFailed(
94+
throw new errors.ErrorPolykeyCLILoginFailed(
8795
'Failed to send token to return url',
88-
{ cause: e, },
96+
{ cause: e },
8997
);
9098
}
99+
91100
// Handle non-200 response
92101
if (!result.ok) {
93-
throw new binErrors.ErrorPolykeyCLILoginFailed(
94-
'Return url returned failure',
95-
{
96-
data: {
97-
code: result.status,
98-
},
99-
},
102+
throw new errors.ErrorPolykeyCLILoginFailed(
103+
`Return url returned failure with code ${result.status}`,
100104
);
101105
}
102106
} finally {

src/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ 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+
199204
class ErrorPolykeyCLILoginFailed<T> extends ErrorPolykeyCLI<T> {
200205
static description = 'Failed to login using Polykey';
201206
exitCode = sysexits.SOFTWARE;
@@ -229,5 +234,6 @@ export {
229234
ErrorPolykeyCLICatSecret,
230235
ErrorPolykeyCLIEditSecret,
231236
ErrorPolykeyCLITouchSecret,
237+
ErrorPolykeyCLIInvalidJWT,
232238
ErrorPolykeyCLILoginFailed,
233239
};

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: 13 additions & 3 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,
@@ -469,7 +469,7 @@ function outputFormatterError(err: any): string {
469469
if (err.data && !utils.isEmptyObject(err.data)) {
470470
output += `${indent}data\t${JSON.stringify(err.data)}\n`;
471471
}
472-
if (err.cause) {
472+
if (err.cause && !utils.isEmptyObject(err.cause)) {
473473
output += `${indent}cause: `;
474474
if (err.cause instanceof ErrorPolykey) {
475475
err = err.cause;
@@ -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)