UNPKG

next-authentication

Version:

Authentication & Authorization Library for the Next.js Framework

791 lines (684 loc) 25.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var crypto = _interopDefault(require('crypto')); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } // Copyright Joyent, Inc. and other Node contributors. // If obj.hasOwnProperty has been overridden, then calling // obj.hasOwnProperty(prop) will break. // See: https://github.com/joyent/node/issues/1707 function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } var decode = function(qs, sep, eq, options) { sep = sep || '&'; eq = eq || '='; var obj = {}; if (typeof qs !== 'string' || qs.length === 0) { return obj; } var regexp = /\+/g; qs = qs.split(sep); var maxKeys = 1000; if (options && typeof options.maxKeys === 'number') { maxKeys = options.maxKeys; } var len = qs.length; // maxKeys <= 0 means that we should not limit keys count if (maxKeys > 0 && len > maxKeys) { len = maxKeys; } for (var i = 0; i < len; ++i) { var x = qs[i].replace(regexp, '%20'), idx = x.indexOf(eq), kstr, vstr, k, v; if (idx >= 0) { kstr = x.substr(0, idx); vstr = x.substr(idx + 1); } else { kstr = x; vstr = ''; } k = decodeURIComponent(kstr); v = decodeURIComponent(vstr); if (!hasOwnProperty(obj, k)) { obj[k] = v; } else if (Array.isArray(obj[k])) { obj[k].push(v); } else { obj[k] = [obj[k], v]; } } return obj; }; // Copyright Joyent, Inc. and other Node contributors. var stringifyPrimitive = function(v) { switch (typeof v) { case 'string': return v; case 'boolean': return v ? 'true' : 'false'; case 'number': return isFinite(v) ? v : ''; default: return ''; } }; var encode = function(obj, sep, eq, name) { sep = sep || '&'; eq = eq || '='; if (obj === null) { obj = undefined; } if (typeof obj === 'object') { return Object.keys(obj).map(function(k) { var ks = encodeURIComponent(stringifyPrimitive(k)) + eq; if (Array.isArray(obj[k])) { return obj[k].map(function(v) { return ks + encodeURIComponent(stringifyPrimitive(v)); }).join(sep); } else { return ks + encodeURIComponent(stringifyPrimitive(obj[k])); } }).join(sep); } if (!name) return ''; return encodeURIComponent(stringifyPrimitive(name)) + eq + encodeURIComponent(stringifyPrimitive(obj)); }; var querystring = createCommonjsModule(function (module, exports) { exports.decode = exports.parse = decode; exports.encode = exports.stringify = encode; }); var querystring_1 = querystring.decode; var querystring_2 = querystring.parse; var querystring_3 = querystring.encode; var querystring_4 = querystring.stringify; function parseBody(req) { var contentType = req.headers["content-type"]; var isForm = contentType === "application/x-www-form-urlencoded"; var isJson = contentType === "application/json"; return new Promise(function (resolve, reject) { if (req.headers["content-type"] === "application/x-www-form-urlencoded") { var body_1 = ""; req.on("data", function (chunk) { body_1 += chunk.toString(); }); req.on("end", function () { if (isForm) { resolve(querystring_2(body_1)); } if (isJson) { resolve(JSON.parse(body_1)); } }); } else { reject(new Error("Content type " + contentType + " not supported")); } }); } var scmpCompare = function scmpCompare (a, b) { var len = a.length; var result = 0; for (var i = 0; i < len; ++i) { result |= a[i] ^ b[i]; } return result === 0; }; /** * Does a constant-time Buffer comparison by not short-circuiting * on first sign of non-equivalency. * * @param {Buffer} a The first Buffer to be compared against the second * @param {Buffer} b The second Buffer to be compared against the first * @return {Boolean} */ var scmp = function scmp (a, b) { // check that both inputs are buffers if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { throw new Error('Both scmp args must be Buffers'); } // return early here if buffer lengths are not equal since timingSafeEqual // will throw if buffer lengths are not equal if (a.length !== b.length) { return false; } // use crypto.timingSafeEqual if available (since Node.js v6.6.0), // otherwise use our own scmp-internal function. if (crypto.timingSafeEqual) { return crypto.timingSafeEqual(a, b); } return scmpCompare(a, b); }; // Arbitrary min length, nothing should shorter than this: var MIN_KEY_LENGTH = 16; var simpleEncryptor = function(opts) { if( typeof(opts) == 'string' ) { opts = { key: opts, hmac: true, debug: false }; } var key = opts.key; var verifyHmac = opts.hmac; var debug = opts.debug; var reviver = opts.reviver; if( !key || typeof(key) != 'string' ) { throw new Error('a string key must be specified'); } if( key.length < MIN_KEY_LENGTH ) { throw new Error('key must be at least ' + MIN_KEY_LENGTH + ' characters long'); } if( reviver !== undefined && reviver !== null && typeof(reviver) != 'function' ) { throw new Error('reviver must be a function'); } // Use SHA-256 to derive a 32-byte key from the specified string. // NOTE: We could alternatively do some kind of key stretching here. var cryptoKey = crypto.createHash('sha256').update(key).digest(); // Returns the HMAC(text) using the derived cryptoKey // Defaults to returning the result as hex. function hmac(text, format) { format = format || 'hex'; return crypto.createHmac('sha256', cryptoKey).update(text).digest(format); } // Encrypts an arbitrary object using the derived cryptoKey and retursn the result as text. // The object is first serialized to JSON (via JSON.stringify) and the result is encrypted. // // The format of the output is: // [<hmac>]<iv><encryptedJson> // // <hmac> : Optional HMAC // <iv> : Randomly generated initailization vector // <encryptedJson> : The encrypted object function encrypt(obj) { var json = JSON.stringify(obj); // First generate a random IV. // AES-256 IV size is sixteen bytes: var iv = crypto.randomBytes(16); // Make sure to use the 'iv' variant when creating the cipher object: var cipher = crypto.createCipheriv('aes-256-cbc', cryptoKey, iv); // Generate the encrypted json: var encryptedJson = cipher.update(json, 'utf8', 'base64') + cipher.final('base64'); // Include the hex-encoded IV + the encrypted base64 data // NOTE: We're using hex for encoding the IV to ensure that it's of constant length. var result = iv.toString('hex') + encryptedJson; if( verifyHmac ) { // Prepend an HMAC to the result to verify it's integrity prior to decrypting. // NOTE: We're using hex for encoding the hmac to ensure that it's of constant length result = hmac(result, 'hex') + result; } return result; } // Decrypts the encrypted cipherText and returns back the original object. // If the cipherText cannot be decrypted (bad key, bad text, bad serialization) then it returns null. // // NOTE: This function never throws an error. It will instead return null if it cannot decrypt the cipherText. // NOTE: It's possible that the data decrypted is null (since it's valid input for encrypt(...)). // It's up to the caller to decide if the result is valid. function decrypt(cipherText) { if( !cipherText ) { return null; } try { if( verifyHmac ) { // Extract the HMAC from the start of the message: var expectedHmac = cipherText.substring(0, 64); // The remaining message is the IV + encrypted message: cipherText = cipherText.substring(64); // Calculate the actual HMAC of the message: var actualHmac = hmac(cipherText); if( !scmp(Buffer.from(actualHmac, 'hex'), Buffer.from(expectedHmac, 'hex')) ) { throw new Error('HMAC does not match'); } } // Extract the IV from the beginning of the message: var iv = Buffer.from(cipherText.substring(0,32), 'hex'); // The remaining text is the encrypted JSON: var encryptedJson = cipherText.substring(32); // Make sure to use the 'iv' variant when creating the decipher object: var decipher = crypto.createDecipheriv('aes-256-cbc', cryptoKey, iv); // Decrypt the JSON: var json = decipher.update(encryptedJson, 'base64', 'utf8') + decipher.final('utf8'); // Return the parsed object: return JSON.parse(json, reviver); } catch( e ) { // If we get an error log it and ignore it. Decrypting should never fail. if( debug ) { console.error('Exception in decrypt (ignored): %s', e); } return null; } } return { encrypt: encrypt, decrypt: decrypt, hmac: hmac }; }; function encrypt(value, secret) { return simpleEncryptor(secret).encrypt(value); } function decrypt(cypherText, secret) { var decrypted = simpleEncryptor(secret).decrypt(cypherText); if (decrypted != null) { return decrypted; } return ""; } var AuthError = /** @class */ (function (_super) { __extends(AuthError, _super); function AuthError(message, status) { if (status === void 0) { status = 401; } var params = []; for (var _i = 2; _i < arguments.length; _i++) { params[_i - 2] = arguments[_i]; } var _this = _super.apply(this, params) || this; if (Error.captureStackTrace) { Error.captureStackTrace(_this, AuthError); } _this.name = "AuthError"; _this.status = status; _this.message = message; return _this; } return AuthError; }(Error)); /*! * cookie * Copyright(c) 2012-2014 Roman Shtylman * Copyright(c) 2015 Douglas Christopher Wilson * MIT Licensed */ /** * Module exports. * @public */ var parse_1 = parse; var serialize_1 = serialize; /** * Module variables. * @private */ var decode$1 = decodeURIComponent; var encode$1 = encodeURIComponent; var pairSplitRegExp = /; */; /** * RegExp to match field-content in RFC 7230 sec 3.2 * * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] * field-vchar = VCHAR / obs-text * obs-text = %x80-FF */ var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; /** * Parse a cookie header. * * Parse the given cookie header string into an object * The object has the various cookies as keys(names) => values * * @param {string} str * @param {object} [options] * @return {object} * @public */ function parse(str, options) { if (typeof str !== 'string') { throw new TypeError('argument str must be a string'); } var obj = {}; var opt = options || {}; var pairs = str.split(pairSplitRegExp); var dec = opt.decode || decode$1; for (var i = 0; i < pairs.length; i++) { var pair = pairs[i]; var eq_idx = pair.indexOf('='); // skip things that don't look like key=value if (eq_idx < 0) { continue; } var key = pair.substr(0, eq_idx).trim(); var val = pair.substr(++eq_idx, pair.length).trim(); // quoted values if ('"' == val[0]) { val = val.slice(1, -1); } // only assign once if (undefined == obj[key]) { obj[key] = tryDecode(val, dec); } } return obj; } /** * Serialize data into a cookie header. * * Serialize the a name value pair into a cookie string suitable for * http headers. An optional options object specified cookie parameters. * * serialize('foo', 'bar', { httpOnly: true }) * => "foo=bar; httpOnly" * * @param {string} name * @param {string} val * @param {object} [options] * @return {string} * @public */ function serialize(name, val, options) { var opt = options || {}; var enc = opt.encode || encode$1; if (typeof enc !== 'function') { throw new TypeError('option encode is invalid'); } if (!fieldContentRegExp.test(name)) { throw new TypeError('argument name is invalid'); } var value = enc(val); if (value && !fieldContentRegExp.test(value)) { throw new TypeError('argument val is invalid'); } var str = name + '=' + value; if (null != opt.maxAge) { var maxAge = opt.maxAge - 0; if (isNaN(maxAge) || !isFinite(maxAge)) { throw new TypeError('option maxAge is invalid') } str += '; Max-Age=' + Math.floor(maxAge); } if (opt.domain) { if (!fieldContentRegExp.test(opt.domain)) { throw new TypeError('option domain is invalid'); } str += '; Domain=' + opt.domain; } if (opt.path) { if (!fieldContentRegExp.test(opt.path)) { throw new TypeError('option path is invalid'); } str += '; Path=' + opt.path; } if (opt.expires) { if (typeof opt.expires.toUTCString !== 'function') { throw new TypeError('option expires is invalid'); } str += '; Expires=' + opt.expires.toUTCString(); } if (opt.httpOnly) { str += '; HttpOnly'; } if (opt.secure) { str += '; Secure'; } if (opt.sameSite) { var sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite; switch (sameSite) { case true: str += '; SameSite=Strict'; break; case 'lax': str += '; SameSite=Lax'; break; case 'strict': str += '; SameSite=Strict'; break; case 'none': str += '; SameSite=None'; break; default: throw new TypeError('option sameSite is invalid'); } } return str; } /** * Try decoding a string using a decoding function. * * @param {string} str * @param {function} decode * @private */ function tryDecode(str, decode) { try { return decode(str); } catch (e) { return str; } } var authenticate = function (handler, options) { return function (req, res) { return __awaiter(void 0, void 0, void 0, function () { var cookieName, cookieUserOptions, secret, verify, cookieDefaultOptions, cookieOptions, _a, username, password, _b, user, token, error_1; var _c, _d; return __generator(this, function (_e) { switch (_e.label) { case 0: cookieName = options.cookieName, cookieUserOptions = options.cookieUserOptions, secret = options.secret, verify = options.verify; cookieDefaultOptions = { SameSite: "lax", secure: process.env.NODE_ENV === "production", httpOnly: true, maxAge: 60 * 60 * 24, path: "/", }; cookieOptions = __assign(__assign({}, cookieDefaultOptions), { cookieUserOptions: cookieUserOptions }); _e.label = 1; case 1: _e.trys.push([1, 6, , 7]); if (!((_c = req.body) !== null && _c !== void 0)) return [3 /*break*/, 2]; _b = _c; return [3 /*break*/, 4]; case 2: return [4 /*yield*/, parseBody(req)]; case 3: _b = (_e.sent()); _e.label = 4; case 4: _a = _b, username = _a.username, password = _a.password; if (!username || !password) { throw new AuthError("Missing credentials"); } return [4 /*yield*/, verify(username, password)]; case 5: user = _e.sent(); token = encrypt(user, secret); res.setHeader("Set-Cookie", serialize_1(cookieName, token, cookieOptions)); return [2 /*return*/, handler(req, res)]; case 6: error_1 = _e.sent(); res.statusCode = (_d = error_1.status) !== null && _d !== void 0 ? _d : 500; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ message: error_1.message })); return [3 /*break*/, 7]; case 7: return [2 /*return*/]; } }); }); }; }; function getCookie(req, name) { if (name === void 0) { name = "next-authentication-token"; } if (req.headers.cookie != null) { var parsedCookie = parse_1(req.headers.cookie); return parsedCookie[name]; } return ""; } function deleteCookie(res) { res.setHeader("Set-Cookie", serialize_1("next-authentication-token", "", { maxAge: -1, path: "/" })); } var authorize = function (handler, options) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return __awaiter(void 0, void 0, void 0, function () { var secret, externalServer, cookieName, redirectOnError, redirectUrl, isApi, req, res, token, userObj, isAuthorized; var _a; return __generator(this, function (_b) { secret = options.secret, externalServer = options.externalServer, cookieName = options.cookieName, redirectOnError = options.redirectOnError, redirectUrl = options.redirectUrl; isApi = args.length > 1; req = isApi ? args[0] : args[0].req; res = isApi ? args[1] : args[0].res; try { token = getCookie(req, cookieName); userObj = externalServer ? {} : decrypt(token, secret); isAuthorized = externalServer ? Boolean(token) : Boolean(token) && Boolean(userObj); if (!isAuthorized) { throw new AuthError("Invalid credentials", 403); } req.user = userObj; req.authorized = isAuthorized; return [2 /*return*/, isApi ? handler(req, res) : handler(args[0])]; } catch (error) { deleteCookie(res); if (isApi) { res.statusCode = (_a = error.status) !== null && _a !== void 0 ? _a : 500; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify({ message: error.message })); return [2 /*return*/]; } // getServerSideProps if (redirectOnError) { res.writeHead(307, { Location: redirectUrl }); res.end(); return [2 /*return*/]; } req.user = JSON.stringify({ username: null }); req.authorized = false; return [2 /*return*/, handler(args[0])]; } return [2 /*return*/]; }); }); }; }; var logout = function (handler, options) { return function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return __awaiter(void 0, void 0, void 0, function () { var isApi, req, res; return __generator(this, function (_a) { isApi = args.length > 1; req = isApi ? args[0] : args[0].req; res = isApi ? args[1] : args[0].res; res.setHeader("Set-Cookie", serialize_1(options.cookieName, "", { maxAge: -1, path: "/" })); req.authorized = false; if (options.redirectOnError) { res.writeHead(302, { location: options.redirectUrl }); res.end(); return [2 /*return*/]; } return [2 /*return*/, isApi ? handler(req, res) : handler(args[0])]; }); }); }; }; function nextAuth(_a) { var verify = _a.verify, secret = _a.secret, _b = _a.externalServer, externalServer = _b === void 0 ? false : _b, _c = _a.cookieName, cookieName = _c === void 0 ? "next-authentication-token" : _c, _d = _a.cookieUserOptions, cookieUserOptions = _d === void 0 ? {} : _d, _e = _a.redirectOnError, redirectOnError = _e === void 0 ? true : _e, _f = _a.redirectUrl, redirectUrl = _f === void 0 ? "/login" : _f; return { authenticate: function (handler) { return authenticate(handler, { verify: verify, secret: secret, cookieName: cookieName, cookieUserOptions: cookieUserOptions, }); }, authorize: function (handler) { return authorize(handler, { secret: secret, externalServer: externalServer, cookieName: cookieName, redirectOnError: redirectOnError, redirectUrl: redirectUrl, }); }, logout: function (handler) { return logout(handler, { cookieName: cookieName, redirectOnError: redirectOnError, redirectUrl: redirectUrl }); }, }; } exports.AuthError = AuthError; exports.nextAuth = nextAuth;