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
2 changes: 1 addition & 1 deletion npmDepsHash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256-ckDhoiFXCuB/abIn/GSYKFHMIwe+7pTHCoTbc3Hsyuo=
sha256-QZafJAHalDT2QQhw4evfJ1fun7saN4Mz08JX8YP4SDQ=
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
"mocked-env": "^1.3.5",
"nexpect": "^0.6.0",
"node-gyp-build": "^4.8.4",
"polykey": "^2.3.5",
"polykey": "^2.4.0",
"shelljs": "^0.8.5",
"shx": "^0.3.4",
"tsx": "^3.12.7",
Expand Down
13 changes: 13 additions & 0 deletions src/auth/CommandAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import CommandLogin from './CommandLogin.js';
import CommandPolykey from '../CommandPolykey.js';

class CommandAuth extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('auth');
this.description('Authentication operations');
this.addCommand(new CommandLogin(...args));
}
}

export default CommandAuth;
113 changes: 113 additions & 0 deletions src/auth/CommandLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type PolykeyClient from 'polykey/PolykeyClient.js';
import type {
TokenPayloadEncoded,
TokenProtectedHeaderEncoded,
TokenSignatureEncoded,
} from 'polykey/tokens/types.js';
import type { IdentityRequestData } from 'polykey/client/types.js';
import CommandPolykey from '../CommandPolykey.js';
import * as binProcessors from '../utils/processors.js';
import * as binParsers from '../utils/parsers.js';
import * as binUtils from '../utils/index.js';
import * as binOptions from '../utils/options.js';
import * as errors from '../errors.js';

class CommandLogin extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('login');
this.description('Login to a platform with Polykey identity');
this.argument(
'<token>',
'Token provided by platform for logging in',
binParsers.parseCompactJWT,
);
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
this.action(async (encodedToken, options) => {
const { default: PolykeyClient } = await import(
'polykey/PolykeyClient.js'
);
const tokensUtils = await import('polykey/tokens/utils.js');
const clientOptions = await binProcessors.processClientOptions(
options.nodePath,
options.nodeId,
options.clientHost,
options.clientPort,
this.fs,
this.logger.getChild(binProcessors.processClientOptions.name),
);
const meta = await binProcessors.processAuthentication(
options.passwordFile,
this.fs,
);

let pkClient: PolykeyClient;
this.exitHandlers.handlers.push(async () => {
if (pkClient != null) await pkClient.stop();
});
try {
pkClient = await PolykeyClient.createPolykeyClient({
nodeId: clientOptions.nodeId,
host: clientOptions.clientHost,
port: clientOptions.clientPort,
options: {
nodePath: options.nodePath,
},
logger: this.logger.getChild(PolykeyClient.name),
});

// Create a JSON representation of the encoded header
const [protectedHeader, payload, signature] = encodedToken;
const incomingTokenEncoded = {
payload: payload as TokenPayloadEncoded,
signatures: [
{
protected: protectedHeader as TokenProtectedHeaderEncoded,
signature: signature as TokenSignatureEncoded,
},
],
};

// Get it verified and signed by the agent
const response = await binUtils.retryAuthentication(
(auth) =>
pkClient.rpcClient.methods.authSignToken({
metadata: auth,
...incomingTokenEncoded,
}),
meta,
);

// Send the returned JWT to the returnURL provided by the initial token
const compactHeader = binUtils.jsonToCompactJWT(response);
const incomingPayload =
tokensUtils.parseTokenPayload<IdentityRequestData>(payload);
let result: Response;
try {
result = await fetch(incomingPayload.returnURL, {
method: 'POST',
body: JSON.stringify({ token: compactHeader }),
});
} catch (e) {
throw new errors.ErrorPolykeyCLILoginFailed(
'Failed to send token to return url',
{ cause: e },
);
}

// Handle non-200 response
if (!result.ok) {
throw new errors.ErrorPolykeyCLILoginFailed(
`Return url returned failure with code ${result.status}`,
);
}
} finally {
if (pkClient! != null) await pkClient.stop();
}
});
}
}

export default CommandLogin;
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CommandAuth.js';
12 changes: 12 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ class ErrorPolykeyCLITouchSecret<T> extends ErrorPolykeyCLI<T> {
exitCode = 1;
}

class ErrorPolykeyCLIInvalidJWT<T> extends ErrorPolykeyCLI<T> {
static description: 'JWT is not valid';
exitCode = sysexits.USAGE;
}

class ErrorPolykeyCLILoginFailed<T> extends ErrorPolykeyCLI<T> {
static description = 'Failed to login using Polykey';
exitCode = sysexits.SOFTWARE;
}

export {
ErrorPolykeyCLI,
ErrorPolykeyCLIUncaughtException,
Expand Down Expand Up @@ -224,4 +234,6 @@ export {
ErrorPolykeyCLICatSecret,
ErrorPolykeyCLIEditSecret,
ErrorPolykeyCLITouchSecret,
ErrorPolykeyCLIInvalidJWT,
ErrorPolykeyCLILoginFailed,
};
2 changes: 2 additions & 0 deletions src/polykey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ async function polykeyMain(argv: Array<string>): Promise<number> {
const { default: CommandBootstrap } = await import('./bootstrap/index.js');
const { default: CommandAgent } = await import('./agent/index.js');
const { default: CommandAudit } = await import('./audit/index.js');
const { default: CommandAuth } = await import('./auth/index.js');
const { default: CommandVaults } = await import('./vaults/index.js');
const { default: CommandSecrets } = await import('./secrets/index.js');
const { default: CommandKeys } = await import('./keys/index.js');
Expand Down Expand Up @@ -181,6 +182,7 @@ async function polykeyMain(argv: Array<string>): Promise<number> {
rootCommand.addCommand(new CommandBootstrap({ exitHandlers, fs }));
rootCommand.addCommand(new CommandAgent({ exitHandlers, fs }));
rootCommand.addCommand(new CommandAudit({ exitHandlers, fs }));
rootCommand.addCommand(new CommandAuth({ exitHandlers, fs }));
rootCommand.addCommand(new CommandNodes({ exitHandlers, fs }));
rootCommand.addCommand(new CommandSecrets({ exitHandlers, fs }));
rootCommand.addCommand(new CommandKeys({ exitHandlers, fs }));
Expand Down
26 changes: 26 additions & 0 deletions src/utils/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const vaultNameRegex = /^(?!.*[:])[ -~\t\n]*$/s;
const secretPathRegex = /^(?!.*[=])[ -~\t\n]*$/s;
const secretPathValueRegex = /^([a-zA-Z_][\w]+)?$/;
const environmentVariableRegex = /^([a-zA-Z_]+[a-zA-Z0-9_]*)?$/;
const base64UrlRegex = /^[A-Za-z0-9\-_]+$/;

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

// Compact JWTs are in xxxx.yyyy.zzzz format where x is the protected
// header, y is the payload, and z is the binary signature.
const parseCompactJWT = (token: string): [string, string, string] => {
// Clean up whitespaces
token = token.trim();

// Confirm part amount
const parts = token.split('.');
if (parts.length !== 3) {
throw new InvalidArgumentError(
'JWT must contain three dot-separated parts',
);
}

// Validate base64 encoding
for (const part of parts) {
if (!part || !base64UrlRegex.test(part)) {
throw new InvalidArgumentError('JWT is not correctly encoded');
}
}

return [parts[0], parts[1], parts[2]];
};

export {
vaultNameRegex,
secretPathRegex,
Expand Down Expand Up @@ -220,4 +245,5 @@ export {
parseProviderId,
parseIdentityId,
parseProviderIdList,
parseCompactJWT,
};
14 changes: 12 additions & 2 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FileSystem } from 'polykey/types.js';
import type { POJO } from 'polykey/types.js';
import type { FileSystem, POJO } from 'polykey/types.js';
import type { SignedTokenEncoded } from 'polykey/tokens/types.js';
import type {
TableRow,
TableOptions,
Expand Down Expand Up @@ -637,6 +637,15 @@ async function importFS(fs?: FileSystem): Promise<FileSystem> {
return fsImported;
}

function jsonToCompactJWT(token: SignedTokenEncoded): string {
if (token.signatures.length !== 1) {
throw new errors.ErrorPolykeyCLIInvalidJWT(
'Too many signatures, expected 1',
);
}
return `${token.signatures[0].protected}.${token.payload}.${token.signatures[0].signature}`;
}

export {
verboseToLogLevel,
standardErrorReplacer,
Expand All @@ -660,6 +669,7 @@ export {
generateVersionString,
promise,
importFS,
jsonToCompactJWT,
};

export type { OutputObject };
Loading