@j2inn/scram
Version:
TypeScript client SCRAM authentication library
383 lines (374 loc) • 16.1 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var sjcl = require('sjcl');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var sjcl__default = /*#__PURE__*/_interopDefaultLegacy(sjcl);
/*
* Copyright (c) 2025, J2 Innovations. All Rights Reserved
*/
var __classPrivateFieldSet$1 = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet$1 = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _ScramError_options;
/**
* The stage in the SCRAM authentication process from the client side.
*/
exports.ScramStage = void 0;
(function (ScramStage) {
/**
* Initialization where client and server agree on the specific SCRAM mechanism to use.
*/
ScramStage["Hello"] = "hello";
/**
* The client sends its username and a client-generated nonce to the server.
*/
ScramStage["FirstMessage"] = "firstMessage";
/**
* The client computes a "Client Proof" then sends this proof to the server.
*/
ScramStage["LastMessage"] = "lastMessage";
/**
* The client validates the Server Signature.
*/
ScramStage["Validation"] = "validation";
})(exports.ScramStage || (exports.ScramStage = {}));
/**
* Error returned by the SCRAM authentication process.
*/
class ScramError extends Error {
/**
* Constructs a {@link ScramError}.
*
* @param message Optional message.
* @param options Additional options to construct an error.
*/
constructor(message, options) {
super(message);
_ScramError_options.set(this, void 0);
__classPrivateFieldSet$1(this, _ScramError_options, options, "f");
}
get response() {
return __classPrivateFieldGet$1(this, _ScramError_options, "f")?.response;
}
get stage() {
return __classPrivateFieldGet$1(this, _ScramError_options, "f")?.stage;
}
/**
* Constructs a {@link ScramError} for the Hello stage.
*
* @param message Optional message.
* @param options Additional options to construct an error.
*/
static hello(message, options) {
return new ScramError(message, {
...options,
stage: exports.ScramStage.Hello,
});
}
/**
* Constructs a {@link ScramError} for the First Message stage.
*
* @param message Optional message.
* @param options Additional options to construct an error.
*/
static firstMessage(message, options) {
return new ScramError(message, {
...options,
stage: exports.ScramStage.FirstMessage,
});
}
/**
* Constructs a {@link ScramError} for the Last Message stage.
*
* @param message Optional message.
* @param options Additional options to construct an error.
*/
static lastMessage(message, options) {
return new ScramError(message, {
...options,
stage: exports.ScramStage.LastMessage,
});
}
/**
* Constructs a {@link ScramError} for the Validation stage.
*
* @param message Optional message.
* @param options Additional options to construct an error.
*/
static validation(message, options) {
return new ScramError(message, {
...options,
stage: exports.ScramStage.Validation,
});
}
}
_ScramError_options = new WeakMap();
/*
* Copyright (c) 2022, J2 Innovations. All Rights Reserved
*/
var __classPrivateFieldSet = (undefined && undefined.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (undefined && undefined.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _ScramAuth_username, _ScramAuth_password, _ScramAuth_authUri, _ScramAuth_dkLen;
const { codec, hash, misc, random } = sjcl__default["default"];
/**
* Key length in bits.
*/
const DK_LEN = {
'SHA-256': 32 * 8,
'SHA-384': 48 * 8,
'SHA-512': 64 * 8,
};
/**
* Performs the client authentication steps required by the SCRAM specification.
*/
class ScramAuth {
/**
* Construct a new authenticator.
*
* @param username The username.
* @param password The password.
* @param uri Server URI to authenticate with.
* @param fetchFunc Alternative fetch function.
*/
constructor(username, password, uri, fetchFunc) {
_ScramAuth_username.set(this, void 0);
_ScramAuth_password.set(this, void 0);
_ScramAuth_authUri.set(this, void 0);
_ScramAuth_dkLen.set(this, void 0);
__classPrivateFieldSet(this, _ScramAuth_username, username, "f");
__classPrivateFieldSet(this, _ScramAuth_password, password, "f");
__classPrivateFieldSet(this, _ScramAuth_authUri, uri, "f");
this.fetch = fetchFunc;
}
/**
* Performs the client authentication
*
* @returns Optional `authToken` in an Record<string, string>
*/
async authenticate() {
const handshakeResponse = await this.hello();
if ('scram' in handshakeResponse) {
return this.scram(handshakeResponse);
}
else {
throw ScramError.hello('Unsupported authentication type');
}
}
/**
* Sends the 'hello' handshake message
* @returns Record<string, string> or error
*/
async hello() {
const response = await this.fetch(__classPrivateFieldGet(this, _ScramAuth_authUri, "f"), {
method: 'GET',
headers: {
'content-Type': 'text/plain',
authorization: 'hello username=' +
codec.base64url.fromBits(codec.utf8String.toBits(__classPrivateFieldGet(this, _ScramAuth_username, "f"))),
},
});
const { headers } = response;
const wwwAuthenticate = headers.get('www-authenticate');
if (!wwwAuthenticate) {
throw ScramError.hello('Invalid authentication headers', {
response,
});
}
const authHeaders = this.parseAuthHeaders(wwwAuthenticate);
const { hash: authHash } = authHeaders;
if (!authHash) {
throw ScramError.hello('Invalid handshake response', {
response,
});
}
__classPrivateFieldSet(this, _ScramAuth_dkLen, DK_LEN[authHash.toUpperCase()], "f");
if (!__classPrivateFieldGet(this, _ScramAuth_dkLen, "f")) {
throw ScramError.hello(`Unsupported hashing scheme: ${authHash}`, {
response,
});
}
if (wwwAuthenticate.toLowerCase().startsWith('scram ')) {
authHeaders['scram'] = 'scram';
}
return authHeaders;
}
/**
* Performs the SCRAM authentication logic.
*
* @param headers The HTTP handshake response headers from the server
*/
async scram(headers) {
const { handshakeToken } = headers;
if (!handshakeToken) {
throw ScramError.firstMessage('Invalid handshake response');
}
const { clientFinal, saltedPassword, authMessage } = await this.clientFirstMessage(handshakeToken);
const finalAuthResponse = await this.clientFinalMessage(clientFinal, handshakeToken);
this.validateFinalResponse(finalAuthResponse, saltedPassword, authMessage);
return finalAuthResponse;
}
async clientFirstMessage(handshakeToken) {
const clientNonce = this.generateClientNonce();
const clientFirstBare = `n=${__classPrivateFieldGet(this, _ScramAuth_username, "f")},r=${clientNonce}`;
const clientFirstMsg = `n,,${clientFirstBare}`;
const data = codec.base64url.fromBits(codec.utf8String.toBits(clientFirstMsg));
// send client first message
const response = await this.fetch(__classPrivateFieldGet(this, _ScramAuth_authUri, "f"), {
method: 'GET',
headers: {
Authorization: `scram handshakeToken=${handshakeToken},data=${data}`,
},
});
const { headers: serverRes1 } = response;
const firstAuthResponse = serverRes1?.get('www-authenticate');
if (!firstAuthResponse) {
throw ScramError.firstMessage('Invalid first response', {
response,
});
}
const authHeaders = this.parseAuthHeaders(firstAuthResponse);
const { data: authData, hash: authHash } = authHeaders;
if (!authData || !authHash) {
throw ScramError.firstMessage('Invalid first response auth headers', {
response,
});
}
const serverFirstMsg = codec.utf8String.fromBits(codec.base64url.toBits(authData));
const serverFirstMap = this.parseAuthHeaders(serverFirstMsg);
const { r: serverNonce, s: salt, i: iterations } = serverFirstMap;
if (!serverNonce || !salt || !iterations) {
throw ScramError.firstMessage('Invalid first server response', {
response,
});
}
if (serverNonce.length < clientNonce.length ||
serverNonce.substring(0, clientNonce.length) !== clientNonce) {
throw ScramError.firstMessage("Client nonce doesn't match server response");
}
const serverSalt = codec.base64.toBits(salt);
const serverIterations = parseInt(iterations);
const dkLen = DK_LEN[authHash.toUpperCase()];
if (!dkLen || dkLen !== __classPrivateFieldGet(this, _ScramAuth_dkLen, "f")) {
throw ScramError.firstMessage(`Unsupported hashing scheme: ${authHash}`);
}
const saltedPassword = misc.pbkdf2(__classPrivateFieldGet(this, _ScramAuth_password, "f"), serverSalt, serverIterations, dkLen);
const clientFinalNoPf = `c=biws,r=${serverNonce}`;
const authMessage = `${clientFirstBare},${serverFirstMsg},${clientFinalNoPf}`;
const clientKey = new misc.hmac(saltedPassword).encrypt('Client Key');
const storedKey = new hash.sha256().update(clientKey).finalize();
const clientSignature = new misc.hmac(storedKey).encrypt(authMessage);
const clientProof = this.xor(clientKey, clientSignature);
const clientFinal = `${clientFinalNoPf},p=${codec.base64.fromBits(clientProof)}`;
return { clientFinal, saltedPassword, authMessage };
}
async clientFinalMessage(clientFinal, handshakeToken) {
const data = codec.base64url.fromBits(codec.utf8String.toBits(clientFinal));
// send client final message
const response = await this.fetch(__classPrivateFieldGet(this, _ScramAuth_authUri, "f"), {
method: 'GET',
headers: {
Authorization: `scram handshakeToken=${handshakeToken},data=${data}`,
},
});
const { headers: serverRes2, status } = response;
if (status !== 200 /*OK*/) {
throw ScramError.lastMessage(`Invalid status response: ${status}`, {
response,
});
}
const finalAuthResponse = serverRes2.get('authentication-info');
if (finalAuthResponse) {
const result = this.parseAuthHeaders(finalAuthResponse);
result['x-csrf-token'] = serverRes2.get('x-csrf-token') ?? '';
return result;
}
else {
throw ScramError.lastMessage('Invalid final response', {
response,
});
}
}
validateFinalResponse(finalAuthResponse, saltedPassword, authMessage) {
const { data: responseData, hash: authHash } = finalAuthResponse;
if (!responseData || !authHash) {
throw ScramError.validation('Invalid final server response');
}
if (DK_LEN[authHash] !== __classPrivateFieldGet(this, _ScramAuth_dkLen, "f")) {
throw ScramError.validation('Invalid hash algorithm');
}
const data = codec.utf8String.fromBits(codec.base64url.toBits(responseData));
const dataMap = this.parseAuthHeaders(data);
const serverSig = dataMap.v;
const serverKey = new misc.hmac(saltedPassword).encrypt('Server Key');
const clientServerSignature = new misc.hmac(serverKey).encrypt(authMessage);
if (serverSig !== codec.base64.fromBits(clientServerSignature)) {
throw ScramError.validation('Authentication failure. Server signature is not valid');
}
}
generateClientNonce() {
return codec.base64url.fromBits(random.randomWords(8));
}
xor(key, sig) {
if (key.length !== sig.length) {
throw new ScramError('Invalid arguments');
}
return key.map((k, index) => k ^ sig[index]);
}
parseAuthHeaders(header) {
return header
.split(/\s+/)
.map((v) => v.split(','))
.flat()
.map((v) => v.split(/(?!\w+)=(?=[A-Za-z\d\/_+-=]+)/))
.filter((v) => v.length > 1)
.reduce((authData, keyValuePair) => {
authData[keyValuePair[0]] = keyValuePair[1];
return authData;
}, {});
}
}
_ScramAuth_username = new WeakMap(), _ScramAuth_password = new WeakMap(), _ScramAuth_authUri = new WeakMap(), _ScramAuth_dkLen = new WeakMap();
/*
* Copyright (c) 2022, J2 Innovations. All Rights Reserved
*/
/**
* Authenticate with a server using SCRAM.
*
* - https://tools.ietf.org/html/rfc5802
* - http://www.alienfactory.co.uk/articles/skyspark-scram-over-sasl
*
* @param options.username The username.
* @param options.password The password.
* @param options.uri Optional server URI to authenticate with (defaults to /).
* @param options.fetch Optional alternative fetch function.
* @returns If successful, the final client authentication information is returned.
* @throws If the authentication is not successful or if there's an error.
*/
async function authenticate({ username, password, uri = '/', fetch = typeof globalThis?.fetch === 'function'
? globalThis.fetch.bind(globalThis)
: undefined, }) {
if (!fetch) {
throw new Error('Fetch is undefined');
}
return await new ScramAuth(username, password, uri, fetch).authenticate();
}
exports.ScramError = ScramError;
exports.authenticate = authenticate;
exports["default"] = authenticate;