aws / aws-sigv4-auth-cassandra-nodejs-driver-plugin

A SigV4 authentication client side plugin for the open-source DataStax NodeJS Driver for Apache Cassandra. Allows use of IAM users and roles.
Apache License 2.0
5 stars 16 forks source link

AWS SDK v3 support #13

Open trangvu9 opened 1 year ago

trangvu9 commented 1 year ago

Any roadmap to migrate to AWS SDK V3?

Please migrate your code to use AWS SDK for JavaScript (v3).                                                  
For more information, check the migration guide at https://a.co/7PzMCcy                                       
     at emitWarning (/data/.../node_modules/aws-sdk/lib/maintenance_mode_message.js:37:13)                
     at Timeout._onTimeout (/data/.../node_modules/aws-sdk/lib/maintenance_mode_message.js:45:5)
bernd-yomali commented 11 months ago

I am also facing this issue

higherorderfunctor commented 10 months ago

I was able to hack one up in typescript for non-temporary access keys (no tokens) using only v3 APIs. Needs additional work, but might start as a base.

/* eslint-disable max-classes-per-file */
import { Sha256 } from '@aws-crypto/sha256-js';
import { createScope, SignatureV4 } from '@smithy/signature-v4';
import { auth, Client as Cassandra } from 'cassandra-driver';
import { createHash } from 'crypto';
import fs from 'fs';
import _ from 'lodash';

class SigV4Authenticator implements auth.Authenticator {
    private readonly region: string;

    private readonly accessKeyId: string;

    private readonly secretAccessKey: string;

    public static readonly AWS4_SIGNING_ALGORITHM = 'AWS4-HMAC-SHA256';

    constructor(props: { region: string; accessKeyId: string; secretAccessKey: string }) {
        this.region = props.region;
        this.accessKeyId = props.accessKeyId;
        this.secretAccessKey = props.secretAccessKey;
    }

    /* Calling class expects to be a method, so cannot make static even though it is a static function */
    /* eslint-disable-next-line class-methods-use-this */
    initialResponse = (callback: (error: Error | null, buffer: Buffer | null) => void): void => {
        callback(null, Buffer.from('SigV4\0\0', 'utf8'));
    };

    private static handleError =
        (callback: (error: Error | null, buffer: Buffer | null) => void) =>
        (err: unknown): void => {
            callback(_.isError(err) ? err : new Error(JSON.stringify(err)), null);
        };

    private handleSignature =
        (timestamp: string, callback: (error: Error | null, buffer: Buffer | null) => void) =>
        (signature: string): void => {
            const payload = `signature=${signature},access_key=${this.accessKeyId},amzdate=${timestamp}`;
            callback(null, Buffer.from(payload, 'utf-8'));
        };

    evaluateChallenge = (challenge: Buffer, callback: (error: Error | null, buffer: Buffer | null) => void) => {
        const res = challenge.toString().split('nonce=');
        if (res.length < 2) {
            callback(new Error(`could not parse nonce: ${challenge.toString()}`), null);
            return;
        }
        const nonce = res[1].split(',')[0];
        const timestamp = new Date().toISOString();
        /* eslint-disable-next-line no-useless-escape */
        const timestampDate = timestamp.replace(/[:\-]|\.\d{3}/g, '').slice(0, 8);
        const nonceHash = Buffer.from(createHash('sha256').update(nonce, 'utf-8').digest()).toString('hex');
        const scope = createScope(timestampDate, this.region, 'cassandra');
        const headers = [
            `X-Amz-Algorithm=${SigV4Authenticator.AWS4_SIGNING_ALGORITHM}`,
            `X-Amz-Credential=${this.accessKeyId}%2F${encodeURIComponent(scope)}`,
            `X-Amz-Date=${encodeURIComponent(timestamp)}`,
            'X-Amz-Expires=900',
        ].sort();
        const canonicalRequest = `PUT\n/authenticate\n${headers.join('&')}\nhost:cassandra\n\nhost\n${nonceHash}`;
        const digest = Buffer.from(createHash('sha256').update(canonicalRequest, 'utf-8').digest()).toString('hex');
        const stringToSign = `${SigV4Authenticator.AWS4_SIGNING_ALGORITHM}\n${timestamp}\n${scope}\n${digest}`;
        const signer = new SignatureV4({
            service: 'cassandra',
            region: this.region,
            credentials: {
                accessKeyId: this.accessKeyId,
                secretAccessKey: this.secretAccessKey,
            },
            sha256: Sha256,
        });
        signer
            .sign(stringToSign)
            .then(this.handleSignature(timestamp, callback))
            .catch(SigV4Authenticator.handleError(callback));
    };

    /* Calling class expects to be a method, so cannot make static even though it is a static function */
    /* eslint-disable-next-line class-methods-use-this */
    onAuthenticationSuccess = (): void => {};
}

class SigV4AuthProvider implements auth.AuthProvider {
    private readonly region: string;

    private readonly accessKeyId: string;

    private readonly secretAccessKey: string;

    constructor(props: { region?: string; accessKeyId: string; secretAccessKey: string }) {
        Object.setPrototypeOf(this, auth.PlainTextAuthProvider.prototype);
        const region = props.region || process.env.AWS_REGION;
        if (_.isNil(region)) throw new Error('no AWS region specified');
        this.region = region;
        this.accessKeyId = props.accessKeyId;
        this.secretAccessKey = props.secretAccessKey;
    }

    newAuthenticator = (): auth.Authenticator =>
        new SigV4Authenticator({
            region: this.region,
            accessKeyId: this.accessKeyId,
            secretAccessKey: this.secretAccessKey,
        });
}

const client = new Cassandra({
    contactPoints: ['cassandra.us-east-1.amazonaws.com'],
    localDataCenter: 'us-east-1',
    authProvider: new SigV4AuthProvider({
        region: 'us-east-1',
        accessKeyId: '<REDACTED>',
        secretAccessKey: '<REDACTED>',
    }),
    sslOptions: {
        ca: [fs.readFileSync('/home/<REDACTED>/.cassandra/sf-class2-root.crt', 'utf-8')],
        host: 'cassandra.us-east-1.amazonaws.com',
        rejectUnauthorized: true,
    },
    protocolOptions: { port: 9142 },
});

const query = 'SELECT * FROM system_schema.keyspaces';

client.execute(query).then((result) => console.log('Row from Keyspaces:', result));
fredarend commented 7 months ago

2024 and this issue is still open.

bhoudu commented 5 months ago

is there any news about that?