(Note that the more restrictive @fortawesome/fontawesome-free share-alike terms are for the fonts themselves, not its CSS (which is under MIT); see also licenses for dev. deps..)
A maintained and expanded fork of https://github.com/braitsch/node-login.
The project name is a portmanteau of "Node" and "login" and is pronounced "noggin" (a colloquial word for "head").
So if you want Node login, use your "nogin"!
- New user account creation
- Email verification/activation
- Secure password reset via email
- Ability to update / delete account
- Session tracking for logged-in users
- Local cookie storage for returning Users
- PBKDF2-based password encryption
- XSRF/CSRF protection
- helmet integration for HTTP headers control
-
sameSite
session cookies - Timing safe login check comparisons
- Throrough internationalization (i18n) and fundamental accessibility (a11y)
- CLI option for managing accounts
- 100% Cypress UI and CLI Testing Coverage
- Tested on Chrome and Firefox but with a Babel/core-js Rollup routine that should allow the code to work in other browsers as well.
While you can see CHANGES (or the 1.0.0 migration guide) to see all of the fixes and enhancements (including security fixes), over the excellent node-login package, the essential change has been to avoid the necessity of directly modifying source. This component has been retooled to allow it to be added as an npm dependency and provided command-line arguments which customize the appearance and behavior to a high degree—and using a config file or CLI flags rather than environmental variables.
You provide the app (through the --router
argument) your Express app
entry file and optionally other arguments. See the next section.
Besides these changes, there have been subsequent additions as well:
- Groups, privilege, and user management
- Additional security
-
Install Node.js (minimum version of 10.4.0) and MongoDB if you haven't already. (Note that while we have provided a generic database adapter that could in theory be used to support other databases, MongoDB is the only currently supported database.)
-
Install the package.
npm install nogin
-
Install the
peerDependencies
. I recommend installing install-peerdeps (npm i -g install-peerdeps
); theninstall-peerdeps nogin
will auto-install the rest. -
Add a
nogin.js
file (see thenogin-sample.js
file) -
Add a
db
directory at your project root (and probably add it to.gitignore
and.npmignore
) -
Use
run-p
(which is made available by thenogin
dependency,npm-run-all
) along with two of your ownpackage.json
scripts, one of which to start the mongo database, and the other to start the nogin server (note thatnodemon
is also provided as a dependency, so you could add that to thenogin
call to watch for changes tonogin
files):
{
"scripts": {
"mongo": "mongod --port=27017 --dbpath=db --bind_ip=127.0.0.1",
"server": "nogin --localScripts --config nogin.js",
"start": "run-p -r mongo server"
}
}
(The -r
flag indicates that an error in one script will lead both to exit.)
Alternatively, you can start MongoDB in its own separate shell:
mongod
...and from within the nogin directory in another terminal, start the server:
nogin --localScripts --config nogin.js
You will most likely also want to use the --router
and possibly --fallback
, as
well as --postLoginRedirectPath /
arguments. See these options for more details.
-
Once the script mentions it is listening (on port 3000 by default), open a browser window and navigate to: http://127.0.0.1:3000/login
-
(For your live site, ensure your
nogin.js
NL_SITE_URL
is pointing to this port.) -
Set yourself as
rootUser
innogin.js
and add groups and privileges and assign groups to users as relevant. Use dots as namespaces in privilege and group names. Note that the built-in groupsnogin.loggedInUsers
andnogin.guests
will apply to all who are logged in or not, respectfully. The privilegs should be additive from the level of guest to logged in user to regular user to root user. -
Check the privileges in your app. You can check the privileges from
req.hasPrivilege(...)
regardless of whether the user is logged in or a guest. You can also get the privileges from/_privs
JavaScript. If you want live results, you can make a GET request to/_privs?format=json
(or just import the helpernogin/hasPrivilege.js
).
MongoDB may end up with a process that interferes with starting a new instance.
On the Mac, you can follow these steps to resolve:
- Get the port
sudo lsof -i :27017
- Then kill by
kill PID
with PID as the result of step 1 (or if necessarykill -2 PID
).
To view as non-embedded HTML or SVG files (for copy-pasteable commands):
When no verbs are added (only flags are supplied), you can see the above for a detailed description of the available flags. We group and summarize these here (all strings unless indicated). See also "Flags available regardless of "verb"" as those also apply as server flags.
-
-c
/--config
- (Defaults to<cwd>/nogin.json
; may also be a JS file.) -
--cwd
(Defaults toprocess.cwd()
.)
You can also look at nogin-sample.js
for how to set these values within
your own nogin.js
config file.
--secret
--NL_EMAIL_USER
--NL_EMAIL_PASS
--NL_EMAIL_HOST
--NL_EMAIL_FROM
--NL_SITE_URL
--fromText
--fromURL
-
--disableXSRF
(Boolean; defaults tofalse
.) -
--noHelmet
(Boolean; defaults tofalse
.) -
--noHostValidation
(Boolean; defaults tofalse
.) -
--RATE_LIMIT
(A number defaulting to 700 for a rate limit.) -
--transferLimit
(A string like "50mb" to be passed toexpress.json()
andexpress.urlencoded()
) -
--csurfOptions
(A string, or, in config, an object; defaults to{cookie: {signed: true, sameSite: "lax"}
; note that if you are on HTTPS, it is recommended to set this to{cookie: {secure: true, signed: true, sameSite: "lax"}
). -
--helmetOptions
(A string, or, in config, an object; defaults to{frameguard: {action: "SAMEORIGIN"}}
). Note thatSAMEORIGIN
is required as theaction
to allownogin
to be used within your site's iframes. -
--sessionOptions
(A string, or, in config, an object; defaults to{resave: true, saveUninitialized: true}
along withcookie: sessionCookieOptions
,secret
, andstore: MongoStore.create({mongoUrl: DB_URL})
-
--sessionCookieOptions
(A string, or, in config, an object; defaults to{sameSite: 'lax'}
)
-
--NS_EMAIL_TIMEOUT
(number of milliseconds, defaulting to 5000) -
--PORT
(number, defaulting to 3000) -
-a
/--adapter
(Defaults to "mongodb", the only current option.) -
--rootUser
- Users who are granted all available privileges to view and edit groups, privileges, and users
-
--requireName
(Default isfalse
.) -
--countryCodes
(Two-letter country codes as JSON array; defaults to codes in/app/server/modules/country-codes.json
.)
-
--localesBasePath
(Defaults toapp/server
; use if need to redefine locale values.)
These should normally not changing, but can be changed to tweak the HTML that is rendered in emails or on the server.
-
--composeResetPasswordEmailView
(Defaults to/app/server/views/composeResetPasswordEmail.js
) -
--composeActivationEmailView
(Defaults to/app/server/views/composeActivationEmail.js
) -
--injectHTML
(No extra HTML is injected by default) -
--favicon
(String path; defaults to blank.)
-
--stylesheet
(String path; defaults to no extra stylesheets being used.) -
--noBuiltinStylesheets
(Boolean, defaults tofalse
)
-
--localScripts
(Boolean, defaults tofalse
) -
--userJS
(None by default) -
--userJSModule
(None by default)
These should primarily only be used with testing:
-
--useESM
(Boolean, defaults tofalse
.) -
--noPolyfill
(Boolean, defaults tofalse
.)
This is for changing the names or behavior of existing routes. See "Adding routes" for supporting additional routes.
-
--postLoginRedirectPath
(Path/URL to which to redirect after login; defaults to home (/home
) (or locale equivalent), but you should probably set it so that it redirects instead to your root (/
). -
--customRoute
(Multiple strings in format<locale>=<route>=<path>
) -
--crossDomainJSRedirects
(Boolean, defaults tofalse
.)
-
-s
/--SERVE_COVERAGE
(Boolean; defaults tofalse
.) -
--staticDir
(One or more string paths) -
--middleware
(One or more middleware to be required) -
--fallback
- If you need a default static file server and not just specific paths, you can add your own file such as follows, adding any desired headers, etc.:
import express from 'express';
/**
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
* @returns {void}
*/
export default function fallback (req, res, next) {
express.static('.', {
setHeaders (resp /* , path, stat */) {
resp.set({
'Content-Security-Policy': `object-src 'none';`
});
}
})(req, res, next);
}
-
--router
- This is where your own (Express) app should be. Note: The following paths should not be set as they are reserved by nogin:- POST:
/login
,/logout
,/home
,/signup
,/accessAPI
,/reset-password
,/lost-password
,/delete
,/reset
- GET:
/login
,/logout
,/home
,/signup
,/accessAPI
,/reset-password
,/activation
,/users
,/coverage
,/groups
,/privileges
,_lang
,_privs
- POST:
/**
* @param {import('express').Application} app
* @param {{userJS: string}} opts
* @returns {void}
*/
export default function router (app, opts) {
// Grab your `nogin.js` options here as needed
console.log('Started with options:', {
...opts,
secret: '<HIDDEN>',
NL_EMAIL_PASS: '<HIDDEN>'
});
app.get('/', (req, _res, next) => {
// To see your user session info (and determine privileges), you can get
// them from `req.sesssion?.user`:
console.log('req.session', {
...req.session?.user,
_id: '<some unique ID>',
name: 'Brett Zamir',
email: '<the user email address>',
user: 'brettz9',
pass: '<the password hash>',
country: 'US',
activationCode: '<activation code hash>',
activated: true,
passVer: 1,
date: 1725865509592,
cookie: '<cookie hash>',
ip: '::ffff:127.0.0.1'
});
// We just use express' approach to point `/` to the path `/index.html`.
req.url = '/index.html';
// Or instead add optional config to conditionally point to any
// Cypress-instrumented entry file:
// req.url = instrumented
// ? '/instrumented/index.html'
// : '/index.html';
// Or point the user to the login page if they are not yet logged in
// req.url = req.session?.user
// ? '/index.html'
// : '/login';
next();
});
}
-
-d
/--JS_DIR
- (Defaults to/app/public
; used for pointing to instrumented path.)
One can also add a verb to nogin
(e.g., nogin read
) which performs a
different behavior from creating a server.
-
help
- Use with one of the verbs below to get help for that command -
read
/view
- View user account(s) -
update
- Update user account(s) -
add
/create
- Create new user account(s) -
remove
/delete
- Remove user account(s) -
listIndexes
- List indexes of the nogin database
Defaults in parentheses:
-
--loggerLocale
("en-US") -
--noLogging
(false
) -
-n
/--DB_NAME
("nogin") -
-t
/--DB_HOST
("127.0.0.1") -
-p
/--DB_PORT
(27017) -
-u
/--DB_USER
-
-x
/--DB_PASS
These are are all multiple
(one to be added for each user being viewed/added/etc.).
Unless notes, all types are strings.
-
--user
(as the default option, the flag--user
can be omitted) --name
--email
-
--country
(Two digit recognized country code) --pass
-
--passVer
(A number indicating the current schema version; should always be set to "1" currently.) -
--date
(A number timestamp for record creation date) -
--activated
(A boolean)
These are shared but will primarily only be of interest in internal nogin testing:
--activationCode
--unactivatedEmail
-
--activationRequestDate
(A number timestamp)
Note that the CLI API for the add
and update
verbs does not currently
perform all validation that the UI does, so you will need to have some
familiarity with nogin internals to be sure to include all required fields.
-
--userFile
- Path to JSON file containing data to populate -
--cwd
- Used withuserFile
-
--all
- Boolean to indicate desire to remove all user records!
-
pnpm i
(local install to get devDeps) pnpm build-docs
- Open
docs/jsdoc/index.html
Questions and suggestions for improvement are welcome.
For developing docs, see DEVELOPING.
- Review
csurf
and usage - Recheck coverage tests
- See about removing
@fortawesome/fontawesome-free
dependency (and if so, rebuild license badges and remove note above about its license) -
Login page
- Provide option for integration within an existing page to avoid need
for separate login page (Ajax)
- Adapt server-side redirect functionality to give Ajax feedback to client so it could instead handle forwarding with a hash.
- Consider possibility of an option to merge with signup page
- Multiple simultaneously-shown login choices (e.g., to use version control or Cloud Storage and a database managing users and privileges)?
- WebSockets with express-ws and jquery-form?
- Optional captcha (see signup below)
- Provide option for integration within an existing page to avoid need
for separate login page (Ajax)
-
Signup/Home pages
- Allow Multiple choices
- Allow adding to "Set up new account" fields (based on a schema?)
(to be injected into
app/server/views/account.js
) to be passed to the server (app/server/routeList.js
) and saved in the database along with other fields (check the user-supplied don't overwrite built-ins) and shown onhome
(also built byaccount.js
) (unless hidden?); not trusting the client-side values of course (could parse server-side-supplied schema for expected types); usejson-editor
?- Plugin system to allow asking for and saving additional data per user. May optionally be able to reject submission, but should instead use an authentication strategy plugin, if it is more fundamental to authentication (since this should generally be safely additive).
- Concept of SharedStorage for grabbing local user data (or on a URL) for populating site profiles/preferences (under control of user, so they can manage their data in one place, and let sites update their own copy (or directly utilize the local copy) when online and checking)
-
Captchas (svg-captcha
(doesn't use easily breakable SVG text, and could convert to image))
- Plugin system for captchas, while potentially allowing saving to and retrieving from database (e.g., for admin-added list of Q&A's), need not be aware of any other co-submitted data like username, password, etc. Unlike "set up new account" plugins, wouldn't need access to user database, but can of course have potential to reject submission.
-
Authentication strategies
- Passkeys
- See about
passport-next
integration- WebSockets with passport?
- Supporting user choice of authentication method
- Different strategies could optionally offer different schemas for:
- User (Registration and edit user)
- Preferences (Tied to user but unlike general content, may possibly of interest across site/application such as desire for dark mode, and unlike most content, would be private and not likely of interest in content queries)
- Login (e.g., to auto-add field for captcha, or
CryptoKey
andCryptoKeyPair
) generation/selection. - Privileges, Privilege Groups, User Groups
- Different strategies could optionally offer different schemas for:
-
BrowserID
to use with a server-side validation
- See https://github.com/jaredhanson/passport-browserid.
- Would presumably need to revive as a browser add-on
- Browser add-on could also expose global locally stored preferences
- Strategy idea: Check host of email domain and insist on
.name
at a reliable host which promises not to give out domains under a minimum fee (as a deterrent for spamming and encouragement for mail address portability); would need to find hosts willing to commit to such a policy. - Add passwordless option
-
Users page
- Ajax pagination
-
Privileges
- Role-based privileges (esp. for reset/delete!) with admin screens
- Hierarchical groups and roles?
-
Multiple group membership allowing multiple roles per group,
including user-customizable roles in addition to built-in ones
such as the "login" privilege; roles per user (without group)
- Make a simple version of groups where groups are auto-created that map to privileges (e.g., a login group), so can easily add a user to a login group rather than needing to first create the group and add the privilege to that group (these could be the built-in groups, along with a few combined ones like visitor/user/admin/superadmin)
- Anticipate privileges that come automatically based on events (e.g., as with StackExchange)
-
Atomic privileges (e.g.,
view-users
as well as more encompassingview
privilege) - IP addressed-based privileges (or exclusions)
- Expiring privileges (or tied into payment subscription or some other event, auto-renewing or not)
- See to-dos in code for methods needing these!
-
Restore
reset
from GET page to POST on the user (admin) page.
-
Restore
- Use within authentication
- Tie into
PaymentRequest
so privilege groups can be tied to payments or subscriptions- npm package for processing/submitting credit info?
- might restrict access to whole site (though more likely just parts) until payment made
- Need to remember to handle case of users added before privilege changes
- Update docs for any privilege additions/config
-
Other pages
- Method to auto-create accessibility-friendly navigation bar, including
login (root), logout, home, signup, and users (the special pages,
'activation', 'lostPassword', 'resetPassword', 'delete', 'reset',
'coverage', should not need to be added). Also add breadcrumbs and
<link rel=next/prev>
.
- Method to auto-create accessibility-friendly navigation bar, including
login (root), logout, home, signup, and users (the special pages,
'activation', 'lostPassword', 'resetPassword', 'delete', 'reset',
'coverage', should not need to be added). Also add breadcrumbs and
- Change POST APIs to GET where expected.
- Add to
/accessAPI
(GET) to explain API and also possibly consolidate to one page? - "blockedIPs" pseudo-group to which one can add IPs (as distinct from "nogin.guests")
- Add types like number, string, and array (of strings) privileges?
- Add "local" boolean flag to privileges (if delivered by default in JavaScript)?
- Add
/user/<username>
(GET) script for admins and others - Add
/group/<groupname>
(GET) script for admins and others - Could make groups hierarchical (multiple? inheritance privileges)
- Allow variant of
localScripts
which uses CDN but generates fallback - Option to email forgotten username (as a workaround, the reset
password email will send this currently, but not if adding an option to
disable the current
uniqueEmails
mode). Alternatively, could allow login by email. Don't want to show username for email in UI though for privacy reasons (more serious than just detecting that the user has an account, this would detect what their account was). - We should already be checking the important items like avoiding existing names, but we should be rejecting bad values of lesser importance on the server-side as we do on the client-side (e.g., non-emails, too short of passwords, etc.)
- Review client-side validation for any other opportunities (e.g., for
any missing
required
fields, etc.) - Switch from
jsdom
todominum
(once latter may be capable), as latter is lighter-weight and we don't need all that jsdom offers; add tests withinjamilih
for the integration - See about minor to-dos in code along the way
- Make email activation (and email) optional (would mitigate some
problems with email detection when current enforcement of unique emails
(proposed
uniqueEmails: true
) is enabled as users concerned with privacy could at least avoid an email that could be sniffed)? - Make server config to make error messages of potential concern
to privacy optional (avoid allowing testing presence of an email
in the system by feedback from lost password (detecting existent
vs. non-existent email)
- Only complete solution is if also allowing signup/update
to an existing email (otherwise those pages could
be used for detection instead).
- We could also make emails optional (beyond activation?)
- While we could make the errors less specific, this can still effectively be sniffed by signing up for different accounts and seeing if they result in some error (they likely shouldn't otherwise)
- In allowing disabling of
uniqueEmails
, require username be provided so will only send reset password for that account
- Info: Optional since many sites might wish to enforce unique emails or identity-by-email, or may simply wish to give users full feedback about whether a lost password email was successfully sent or not.
- Info: Note that login would always allow detecting existent vs. non-existent user names (this is just for email detection)
- More validation from CLI, e.g., adding an option or default to report if an email is already in use
- Only complete solution is if also allowing signup/update
to an existing email (otherwise those pages could
be used for detection instead).