graphile-settings
Version:
graphile settings
185 lines (176 loc) • 9.32 kB
JavaScript
// import Networks from '@pyramation/crypto-networks';
// import { verifyMessage } from '@pyramation/crypto-keys';
import { context as grafastContext, lambda, object } from 'grafast';
import { extendSchema, gql } from 'graphile-utils';
import { QuoteUtils } from '@pgsql/quotes';
import pgQueryWithContext from 'pg-query-context';
const SAFE_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
const SAFE_CRYPTO_NETWORK = /^[a-z0-9_-]{1,64}$/i;
function validateIdentifier(name, label) {
if (!SAFE_IDENTIFIER.test(name)) {
throw new Error(`PublicKeySignature: invalid ${label} "${name}" — must match /^[a-z_][a-z0-9_]*$/`);
}
}
function validateCryptoNetwork(name) {
if (!SAFE_CRYPTO_NETWORK.test(name)) {
throw new Error('PublicKeySignature: invalid crypto_network — must match /^[a-z0-9_-]{1,64}$/i');
}
}
const MAX_PUBLIC_KEY_LENGTH = 256;
const MAX_MESSAGE_LENGTH = 4096;
const MAX_SIGNATURE_LENGTH = 1024;
const ENABLE_SIGNATURE_VERIFICATION = process.env.ENABLE_SIGNATURE_VERIFICATION === 'true';
export const PublicKeySignature = (pubkey_challenge) => {
const { schema, crypto_network, sign_up_with_key, sign_in_request_challenge, sign_in_record_failure, sign_in_with_challenge } = pubkey_challenge;
validateIdentifier(schema, 'schema');
validateIdentifier(sign_up_with_key, 'sign_up_with_key');
validateIdentifier(sign_in_request_challenge, 'sign_in_request_challenge');
validateIdentifier(sign_in_record_failure, 'sign_in_record_failure');
validateIdentifier(sign_in_with_challenge, 'sign_in_with_challenge');
validateCryptoNetwork(crypto_network);
return extendSchema(() => ({
typeDefs: gql `
input CreateUserAccountWithPublicKeyInput {
publicKey: String!
}
input GetMessageForSigningInput {
publicKey: String!
}
input VerifyMessageForSigningInput {
publicKey: String!
message: String!
signature: String!
}
type createUserAccountWithPublicKeyPayload {
message: String!
}
type getMessageForSigningPayload {
message: String!
}
type verifyMessageForSigningPayload {
access_token: String!
access_token_expires_at: Datetime!
}
extend type Mutation {
createUserAccountWithPublicKey(
input: CreateUserAccountWithPublicKeyInput
): createUserAccountWithPublicKeyPayload
getMessageForSigning(
input: GetMessageForSigningInput
): getMessageForSigningPayload
verifyMessageForSigning(
input: VerifyMessageForSigningInput
): verifyMessageForSigningPayload
}
`,
plans: {
Mutation: {
createUserAccountWithPublicKey(_$mutation, fieldArgs) {
const $input = fieldArgs.getRaw('input');
const $withPgClient = grafastContext().get('withPgClient');
const $combined = object({ input: $input, withPgClient: $withPgClient });
return lambda($combined, async ({ input, withPgClient }) => {
if (!input.publicKey || typeof input.publicKey !== 'string' || input.publicKey.length > MAX_PUBLIC_KEY_LENGTH) {
throw new Error('INVALID_PUBLIC_KEY');
}
return withPgClient(null, async (pgClient) => {
await pgClient.query('BEGIN');
try {
await pgQueryWithContext({
client: pgClient,
context: { role: 'anonymous' },
query: `SELECT * FROM ${QuoteUtils.quoteQualifiedIdentifier(schema, sign_up_with_key)}($1)`,
variables: [input.publicKey],
skipTransaction: true
});
const { rows: [{ [sign_in_request_challenge]: message }] } = await pgQueryWithContext({
client: pgClient,
context: { role: 'anonymous' },
query: `SELECT * FROM ${QuoteUtils.quoteQualifiedIdentifier(schema, sign_in_request_challenge)}($1)`,
variables: [input.publicKey],
skipTransaction: true
});
await pgClient.query('COMMIT');
return { message };
}
catch (err) {
await pgClient.query('ROLLBACK');
throw err;
}
});
});
},
getMessageForSigning(_$mutation, fieldArgs) {
const $input = fieldArgs.getRaw('input');
const $withPgClient = grafastContext().get('withPgClient');
const $combined = object({ input: $input, withPgClient: $withPgClient });
return lambda($combined, async ({ input, withPgClient }) => {
if (!input.publicKey || typeof input.publicKey !== 'string' || input.publicKey.length > MAX_PUBLIC_KEY_LENGTH) {
throw new Error('INVALID_PUBLIC_KEY');
}
return withPgClient(null, async (pgClient) => {
const { rows: [{ [sign_in_request_challenge]: message }] } = await pgQueryWithContext({
client: pgClient,
context: { role: 'anonymous' },
query: `SELECT * FROM ${QuoteUtils.quoteQualifiedIdentifier(schema, sign_in_request_challenge)}($1)`,
variables: [input.publicKey]
});
if (!message)
throw new Error('NO_ACCOUNT_EXISTS');
return { message };
});
});
},
// NOTE: Verification remains behind a feature flag until crypto
// verification is re-implemented.
verifyMessageForSigning(_$mutation, fieldArgs) {
const $input = fieldArgs.getRaw('input');
const $withPgClient = grafastContext().get('withPgClient');
const $combined = object({ input: $input, withPgClient: $withPgClient });
return lambda($combined, async ({ input, withPgClient }) => {
const { publicKey, message, signature: _signature } = input;
if (!publicKey || typeof publicKey !== 'string' || publicKey.length > MAX_PUBLIC_KEY_LENGTH) {
throw new Error('INVALID_PUBLIC_KEY');
}
if (!message || typeof message !== 'string' || message.length > MAX_MESSAGE_LENGTH) {
throw new Error('INVALID_MESSAGE');
}
if (!_signature || typeof _signature !== 'string' || _signature.length > MAX_SIGNATURE_LENGTH) {
throw new Error('INVALID_SIGNATURE');
}
if (!ENABLE_SIGNATURE_VERIFICATION) {
// Fail closed without mutating lockout counters while verification
// is disabled.
throw new Error('FEATURE_DISABLED');
}
return withPgClient(null, async (pgClient) => {
// Only the success path needs a transaction (multi-step)
await pgClient.query('BEGIN');
try {
const { rows: [token] } = await pgQueryWithContext({
client: pgClient,
context: { role: 'anonymous' },
query: `SELECT * FROM ${QuoteUtils.quoteQualifiedIdentifier(schema, sign_in_with_challenge)}($1, $2)`,
variables: [publicKey, message],
skipTransaction: true
});
if (!token?.access_token)
throw new Error('BAD_SIGNIN');
await pgClient.query('COMMIT');
return {
access_token: token.access_token,
access_token_expires_at: token.access_token_expires_at
};
}
catch (err) {
await pgClient.query('ROLLBACK');
throw err;
}
});
});
}
}
}
}));
};
export default PublicKeySignature;