GithubHelp home page GithubHelp logo

perfood / couch-auth Goto Github PK

View Code? Open in Web Editor NEW
66.0 11.0 19.0 1.75 MB

Powerful authentication for APIs and apps using CouchDB (or Cloudant) with Node >= 14

License: MIT License

JavaScript 4.59% TypeScript 92.77% Nunjucks 2.64%
authentication couchdb cloudant pouchdb offline-first nodejs rxdb

couch-auth's Introduction

CouchAuth

Known Vulnerabilities Build Status

This is a heavily modified SuperLogin, re-written in TypeScript and developed with Node 14/16 & CouchDB 3. It is compatible with Cloudant when using the CouchDB-style authentication, adapted for current OWASP best practises and can be used on CloudFoundry.

Important breaking changes, see the Changelog for details:

  • 0.17.0: Replaced ejs with nunjucks, new templating logic, Node >= 14
  • 0.14.0: Moved db and sl-users - structure to UUIDs

Important notes from the maintainer:

  • I assume that the express server using couch-auth runs behind a load balancer which handles rate limiting. Use something like HAProxy which is also recommended by CouchDB and configure it to prevent brute force attacks.
  • I'm only actively working on / testing the local email/PW authentication strategy, not for the OAuth part. Feel free to use it and to contribute, but you're on your own.

If you encounter a bug, open an issue. If you have trouble setting things up or any other question about the package, join the discussion instead.

Check the Project board for upcoming changes or if you want to contribute.

Below is the (partially adjusted) original README:

Overview

CouchAuth is a full-featured NodeJS/Express user authentication solution for APIs and Single Page Apps (SPA) using CouchDB or Cloudant.

User authentication is often the hardest part of building any web app, especially if you want to integrate multiple providers. Now all the tough work has been done for you so you can relax and create with less boilerplate!

Contents

Features

  • Authentication solution for APIs, sPAs and Offline-First CouchDB powered Apps
  • Supports local login with username/email and password using best security practices
  • Sends system emails for account confirmation, password reset, or anything else you want to configure
  • Add any Passport OAuth2 strategy with literally just a couple lines of code
  • Link multiple authentication strategies to the same account for user convenience
  • Provides seamless token access to both your CouchDB server (or Cloudant) and your private API
  • Manages permissions on an unlimited number of private or shared user databases and seeds them with the correct design documents
  • Enable slowing down requests to /login on errors to prevent brute force attacks

How It Works

Simply authenticate yourself with CouchAuth using any supported strategy and you will be issued a temporary access token and password. Then include the access token and password in an Authorization Bearer header on every request to access protected endpoints. The same credentials (using Basic rather than Bearer Authorization) will authenticate you on any CouchDB or Cloudant database you have been authorized to use.

Quick Start

Here's a simple minimalist configuration that will get you up and running right away:

First:

npm install @perfood/couch-auth express body-parser morgan

You'll need an email service that is supported by nodemailer. Then start a server with the following content:

var express = require('express');
var bodyParser = require('body-parser');
var logger = require('morgan');
var { CouchAuth } = require('@perfood/couch-auth');

var app = express();
app.set('port', process.env.PORT || 3000);
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

var config = {
  dbServer: {
    protocol: 'http://',
    host: 'localhost:5984',
    user: 'admin',
    password: 'password',
    userDB: 'sl-users',
    couchAuthDB: '_users'
  },
  // uncomment this if you want your users to select their own username an login with the username
  // local: {
  //   emailUsername: false, // store the username in the database instead of an auto-generated key
  //   usernameLogin: true, // allow login with username
  // },
  mailer: {
    fromEmail: '[email protected]',
    options: {
      service: 'Gmail', // N.B.: Gmail won't work out of the box, see https://nodemailer.com/usage/using-gmail/
      auth: {
        user: '[email protected]',
        pass: 'userpass'
      }
    }
  },
  userDBs: {
    defaultDBs: {
      private: ['supertest']
    }
  }
};

// Initialize CouchAuth
var couchAuth = new CouchAuth(config);

// Mount CouchAuth's routes to our app
app.use('/auth', couchAuth.router);
app.listen(app.get("port"));

Enabling login via username instead of via email is only recommended if the usernames are public anyways. Otherwise, using email only is more secure and prevents account guessing. Read the OWASP Cheat Sheet for more information.

Now let's create our first user by sending a POST request with the following JSON content to http://localhost:3000/auth/register. Replace the example E-Mail with one that you can access:

{
  "name": "Joe Smith",
  "email": "[email protected]",
  "password": "bigsecret",
  "confirmPassword": "bigsecret"
}

e.g. via curl:

curl --request POST \
  --url http://localhost:3000/auth/register \
  --header 'Content-Type: application/json' \                 
  --data '{"name": "Joe Smith", "email": "[email protected]", "password": "bigsecret", "confirmPassword": "bigsecret"}'

Using x-www-form-urlencoded is also supported:

curl --request POST \
  --url http://localhost:3000/auth/register \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'name=Joe Smith' \
  --data-urlencode '[email protected]' \
  --data-urlencode 'password=bigsecret' \
  --data-urlencode 'confirmPassword=bigsecret'

You should get the response {"success": "Request processed."} and an confirmation E-Mail should have been sent out. Click on the confirmation link to activate your account. You can also manually confirm the user's email by removing the unverifiedEmail - property in his doc in sl-users and adding "email": "[email protected]" instead.

Now to login, simply post your username and password to http://localhost:3000/auth/login. You should get a response similar to this:

{
  "issued": 1440232999594,
  "expires": 1440319399594,
  "provider": "local",
  "token": "aViSVnaDRFKFfdepdXtiEg",
  "password": "p7l9VCNbTbOVeuvEBhYW_A",
  "user_id": "joesmith",
  "roles": ["user"],
  "userDBs": {
    "supertest": "http://aViSVnaDRFKFfdepdXtiEg:p7l9VCNbTbOVeuvEBhYW_A@localhost:5984/supertest$joesmith"
  }
}

You have now been issued an access token. Let's use it to access a protected endpoint. Make a request to http://localhost:3000/auth/refresh and you'll see it was unauthorized. Now add a header to your request: "Authorization": "Bearer {token}:{password}" and you should see that your session was refreshed. That was easy!

If your user document contains a field called profile, this will automatically be included with the session information.

You can also use the same token and password combination to access your personal database. But as soon as you log out your session, that access will be revoked.

Note: Session tokens for your API will be unusable as soon as they expire. However, there is no mechanism to automatically revoke expired credentials with CouchDB. Whenever a user logs in, logs out, or refreshes the session, CouchAuth will automatically clean up any expired credentials for that user. But you have to periodically run couchAuth.removeExpiredKeys(), e.g. with setInterval or a cron job. This will deauthorize every single expired credential.

Securing Your Routes

Securing your routes is very simple:

app.get(
  '/admin',
  couchAuth.requireAuth,
  couchAuth.requireRole('admin'),
  function (req, res) {
    res.send('Welcome Admin');
  }
);

Note that you must use requireAuth prior to checking any roles or an error will be thrown.

couchAuth.requireAuth

Middleware that authenticates a user with a token and password in the request header. ("Authorization": "Bearer {token}:{password}")

couchAuth.requireRole(role)

Middleware that makes sure the authenticated user possesses the specified role (string).

couchAuth.requireAnyRole(possibleRoles)

Middleware that makes sure the user possesses at least one of the specified possibleRoles (array).

couchAuth.requireAllRoles(requiredRoles)

Middleware that makes sure the user possesses ALL of the specified requiredRoles (array).

Database Security

When using CouchDB, you should block anonymous reads across all databases by setting require_valid_user to true under [couch_httpd_auth] in your CouchDB config.

For CouchDB versions < 3, Admin Party is default and all your databases are readable and writable by the public until you implement the correct security measures.

CouchAuth also allows you to specify default _security roles for members and admins in the userDBs section of your config file. See config.example.js for details.

Email templates

Nunjucks is used for the email and oauth callback templates. The defaults in the templates folder. Set emailTemplates.folder accordingly when providing your own templates. For each template defined in emailTemplates.templates, you have two options of including it with couch-auth by placing it into emailTemplates.folder:

  1. Provide a ${template}.html.njk and/or ${template}.text.njk file
  2. Provide a base.njk HTML template and a ${template}.njk file containing the text

When using option 2), you'll have pretty HTML emails with little maintenance overhead:

  • Use line breaks to start new paragraphs
  • Use *..* or _..._, **...**, [desc](url) for basic markdown styling

The base.njk needs to contain a block like this for every paragraph that will be rendered into it:

{% block content %} 
 {% for paragraph in paragraphs %}
   <p>{{paragraph | safe}}</p> 
 {% endfor %}
{% endblock %}

Be sure to never use safe for data that is passed via req inside your nunjucks templates!

You can pass additional data for all templates via emailTemplates.data or for a single template via its data entry. It will be available in nunjucks as data. .... The ${template} is available under templateId.

Support for ejs has been dropped with version 0.17.0.

CouchDB Document Update Validation

CouchDB provides the validate_doc_update function to approve or disapprove what gets written. However, since your CouchDB users are temporary random API keys, you have no idea which user is requesting to write. CouchAuth has inserted the original user_id into userCtx.roles[0], prefixed by user: (e.g. user:superman).

Adding Providers

You can add support for any Passport OAuth2 strategy to CouchAuth with just a few lines of code. Maintainers Note: haven't tested this.

Configuration

The first step is to add credentials to your config file. You can skip the callback URL as it will be generated automatically. Here is how to add support for Dropbox:

providers: {
  dropbox: {
    // Credentials here will be passed in on the call to passport.use
    credentials: {
      consumerKey: DROPBOX_APP_KEY,
      consumerSecret: DROPBOX_APP_SECRET
    },
    options: {
      // Options here will be passed in on the call to passport.authenticate
    },
    // You should copy the template from this repo that is in `templates/oauth/authCallback.njk` and modify the second parameter
    // from '*' to your page origin, e.g. 'https://example.com', to avoid any malicious site receiving the auth data returned by the pop-up
    // window workflow. The template can be the same for all providers.
    template: path.join(__dirname, './templates/oauth/my-custom-secure-authCallback.njk')
  }
}

CouchAuth supports two types of workflows for OAuth2 providers: popup window and client access token.

Popup Window Workflow for web browsers (desktop and mobile)

Your client must create a popup window and point it to /{provider}, where the user will be directed to authenticate with that provider. After authentication, succeeds or fails, it will post a message to the parent window with the data set to {error, session, link }.

In the parent window add an event listener to wait for the message, e.g:

window.addEventListener('message', (event) => {
  if (event.origin !== "http://auth.example.org:3000") { return; }
  // event.data on success contains
  // {
  //   "error": null,
  //   "session": {
  //     "issued": 1624591356009,
  //     "expires": 1624677756009,
  //     "provider": "google",
  //     ...
  //   },
  //   "link": null
  // }
  console.log(event);
  
}, false);

After completing the configuration step above, all you have to do is register your new provider with CouchAuth. Simply follow this pattern:

var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
couchAuth.registerOAuth2('dropbox', DroboxStrategy);

Now, assuming your credentials are valid, you should be able to authenticate with Dropbox by opening a popup window to /dropbox. See below in the Routes documentation for more detail.

Client Access Token for Cordova / Phonegap and Native Apps

Cordova and most native app frameworks (including iOS and Android) have plugins which authenticate a user with a provider and provide an access_token to the client app. All you have to do is post a request to /{provider}/token and include your access_token in the request body. CouchAuth will respond with a new session or an error message.

You must use Passport strategies that accept access_token posted in the body of the request, such as passport-facebook-token, passport-google-token, etc.

Here is how to setup the Client Access Token strategy:

var FacebookTokenStrategy = require('passport-facebook-token');
couchAuth.registerTokenProvider('facebook', FacebookTokenStrategy);

Note that this uses the exact settings in your config as the popup window workflow.

Adding additional fields

It's easy to add custom fields to user documents. When added to a profile field it will automatically be included with the session information (in a profile object).

  1. First whitelist the fields in the config, for example:

    userModel: {
      whitelist: ['profile.fullname'];
    }
  2. Include the fields with registrations.

  3. To also fill in custom fields after social authentications use the onCreate handler. Example:

    couchAuth.onCreate(function (userDoc, provider) {
      if (userDoc.profile === undefined) {
        userDoc.profile = {};
      }
      if (provider !== 'local') {
        const displayName = userDoc[provider].profile.displayName;
        if (displayName) {
          userDoc.profile.fullname = displayName;
        }
      }
      return Promise.resolve(userDoc);
    });

Brute force protection

To enable brute force protection for the /login route you just need to add loginRateLimit: {} to security in your config. The same goes for the /password-reset route, where you just need to add passwordResetRateLimit: {} accordingly. Adding just the empty object uses following defaults that can be overriden as needed:

const config {

  ...

  security: {
    
    ...

    loginRateLimit: {
      windowMs: 5 * 60 * 1000,
      delayAfter: 3,
      delayMs: 500
      maxDelayMs: 10000,
      skipSuccessfulRequests: true,
      skipFailedRequests: false,
      onLimitReached: function () {},
      store: undefined, // if undefined uses Memory Store by default
      headers: false
    }
  }
}

couch-auth uses express-slow-down under the hood, feel free to check the docs to dig deeper into configuration options.

Important notes:

  • You won't be able to override the keyGenerator option, as we use usernameField from the config.
  • When activating rate limiting for the /password-reset route, username field is required in the request body!
  • If you want to use Redis Store instead of Memory Store you currently need to use rate-limit-redis@2x for now due to known issues with newer versions of rate-limit-redis.

Advanced Configuration

Take a look at config.example.js or src/types/config.d.ts for a complete tour of all available configuration options. You'll find a lot of cool hidden features there that aren't documented here.

src/config/default.config.ts contains a list of default settings that will be assumed if you don't specify anything.

Routes

POST /register

Creates a new account with a username and password. Required fields are: username, email, password and confirmPassword. name is optional. Any additional fields you want to include need to be white listed under userModel in your config. See src/config/default.config.ts, config.example.js or src/types/config.d.ts for details.

If local.sendConfirmEmail is true (recommended), a confirmation email will be sent with a verification link. If local.requireEmailConfirm is true, (recommended) the user will not be able to login until the confirmation is complete. If security.loginOnRegistration is true (discouraged), a session will be automatically created and sent as the response. If local.keepEmailConfirmToken is true, the confirmation link will also return 200 if the link is opened multiple times.

POST /login

Include username and password fields to authenticate and initiate a session. The field names can be customized in your config under local.usernameField and local.passwordField.

GET /confirm-email/{token}

This link is included in the confirmation email, and will mark the user as confirmed. If local.confirmEmailRedirectURL is specified in your config, it will redirect to that location with ?success=true if successful or error={error}&message={msg} if it failed. Otherwise it will generate a standard JSON response.

POST /refresh

Authentication token required. Extends the life of your current token and returns updated token information. The only field that will change is expires. Token life is configurable under security.sessionLife and is measured in seconds.

POST /logout

Authentication required. Logs out the current session and deauthorizes the token on all user databases.

POST /logout-others

Authentication required. Logs out and deauthorizes all user sessions except the current one.

POST /logout-all

Authentication required. Logs out every session the user has open and deauthorizes the user completely on all databases.

POST /forgot-password

Include email field to send the forgot password email containing a password reset token. The life of the token can be set under security.tokenLife (in seconds).

Have the email template redirect back to you're app where you're app presents U.I. to gather a new password and then POST to /password-reset with the forgot-password token and new password

POST /password-reset

Resets the password. Required fields: token, password, and confirmPassword. If security.passwordResetRateLimit is set, username (or your configured username field) must be provided as for /login.

POST /password-change

Authentication required. Changes the user's password or creates one if it doesn't exist. Required fields: newPassword, and confirmPassword. If the user already has a password set then currentPassword is required.

GET /validate-username/{username} (deprecated)

Deprecated

Checks a username to make sure it is correctly formed and not already in use. Responds with status 200 if successful, or status 409 if unsuccessful.

GET /validate-email/{email} (deprecated)

Deprecated

Checks an email to make sure it is valid and not already in use. Responds with status 200 if successful, or status 409 if unsuccessful.

POST /change-email

Authentication required. Changes the user's email. Required field: newEmail.

If requirePasswordOnEmailChange is true: The username (can also be email) and password are also required.

Note: The server returns an answer once the email has been verified as valid and whether this email already exists in the DB, not waiting for the update of the email to complete.

GET /session

Deprecated. Simply attempt to access the (user's) CouchDB / instead.

Returns information on the current session if it is valid. Otherwise you will get a 401 unauthorized response. With 2.0, this route shouldn't be used anymore but is still present for backwards compatibility. You should handle session expiration dates on client side, simply try to connect with the Database and handle 401/403 responses accordingly.

POST /request-deletion

Authentication required. A valid login (i.e. email, username or UUId) must be provided as username and the current password. Removes the user's account and all its private databases.

GET /{provider}

Open this in a popup window to initiate authentication with Facebook, Google, etc. After authentication, the callback will post a message to the the parent window with the data object: error explains anything that went wrong, session includes the same session object that is generated by /login and link simply contains the name of the provider that was successfully linked.

GET /link/{provider}?bearer_token={token:password}

This popup window is opened by a user that is already authenticated in order to link additional providers to the account.

There is a security concern here that the session token is exposed as a query parameter in the URL. While this is secure from interception under HTTPS, it can be stored in the user's browser history and your server logs. If you are concerned about this you can either force your user to log out the session after linking an account, or disable link functionality completely by setting security.disableLinkAccounts to true.

POST /unlink/{provider}

Authentication required. Removes the specified provider from the user's account. Local cannot be removed. If there is only one provider left it will fail.

POST /{provider}/token

This will invoke the client access_token strategy for the specified provider if you have registered it. You should include the access_token for the provider in the body of your request.

POST /link/{provider}/token

This will link additional providers to an already authenticated user using the client access_token strategy.

Event Emitter

CouchAuth also provides an event emitter, which allows you to receive notifications when important things happen.

Example:

couchAuth.emitter.on('login', function (userDoc, provider) {
  console.log('User: ' + userDoc._id + ' logged in with ' + provider);
});

Here is a full list of the events that CouchAuth emits, and parameters provided:

  • signup: (userDoc, provider)
  • signup-attempt: (userDoc, provider) // currently only for local
  • link-social: (userDoc, provider)
  • login: (newSession, provider)
  • refresh: (newSession)
  • password-reset: (userDoc)
  • password-change: (userDoc)
  • forgot-password: (userDoc)
  • forgot-password-attempt: (email)
  • email-verified: (userDoc)
  • email-changed: (userDoc)
  • illegal-email-change: (login, newEmail)
  • user-db-added: (dbName)
  • user-db-removed: (dbName)
  • user-deleted: (userDoc, reason)
  • logout: (user_id)
  • logout-all: (user_id)
  • consents: (userDoc)
  • confirmation-email-error: (userDoc)

Main API

new CouchAuth(config, couchServer, passport)

Constructs a new instance of CouchAuth. All arguments are optional. If you don't supply any config object, default settings will be used for a local CouchDB instance in admin party mode. Emails will be logged to the console but not sent.

  • config: Your full configuration object.
  • couchServer: You can pass a ServerScope from @cloudant/cloudant or your own customized version of nano here to make the requests to your CouchDB/Cloudant-instance. Typing issues can be ignored as long as the relevant methods work as in nano. If you don't pass a ServerScope, your installed nano-Version must be >=9.
  • passport: You can pass in your own instance of Passport or CouchAuth will generate one if you do not.

Returns: the complete CouchAuth API.

couchAuth.config

A reference to the configuration object. You can use this to lookup and change configuration settings at runtime. See src/types/config.d.ts for details.

couchAuth.router

A reference to the Express Router that contains all of CouchAuth's routes.

couchAuth.passport

A reference to Passport

couchAuth.events

A reference to the event emitter

couchAuth.userDB

A nano instance that gives direct access to the CouchAuth users database

couchAuth.couchAuthDB

A nano instance that gives direct access to the CouchDB authentication (_users) database.

couchAuth.registerProvider(provider, configFunction)

Adds support for additional Passport strategies. See below under Adding Providers for more information.

couchAuth.validateUsername(username)

Checks that a username is valid and not in use. Resolves with nothing if successful. Resolves with an error object in failed.

couchAuth.validateEmail(email)

Checks that an email is valid and not in use. Resolves with nothing if successful. Resolves with an error object in failed.

couchAuth.getUser(login)

Fetches a user document by either username, email or UUID.

couchAuth.createUser(form, req)

Creates a new local user with a username and password.

form requires the following: username, email, password, and confirmPassword. name is optional. Any additional fields must be whitelisted in your config under userModel or they will be removed.

req should contain protocol and headers.host to properly generate the confirmation email link. ip will be logged if given.

couchAuth.onCreate(fn)

Use this to add as many functions as you want to transform the new user document before it is saved. Your function should accept two arguments (userDoc, provider) and return a Promise that resolves to the modified user document. onCreate functions will be chained in the order they were added.

couchAuth.onLink(fn)

Does the same thing as onCreate, but is called every time a user links a new provider, or their profile information is refreshed. This allows you to process profile information and, for example, create a master profile. If an object called profile exists inside the user doc it will be passed to the client along with session information at each login.

couchAuth.createUserSocial(provider, auth, profile)

Creates a new user following authentication from an OAuth provider. If the user already exists it will update the profile.

  • provider: the name of the provider in lowercase, (e.g. 'facebook')
  • auth: credentials supplied by the provider
  • profile: the profile supplied by the provider
couchAuth.linkUserSocial(login, provider, auth, profile)

like createUserSocial, but for an existing user identified by login

couchAuth.unlinkUserSocial(login, provider)

Removes the specified provider from the user's account. local cannot be removed. If there is only one provider left it will fail.

couchAuth.hashPassword(password)

Hashes a password using PBKDF2 and returns an object containing salt and derived_key.

couchAuth.verifyPassword(hashObj, password)

Verifies a password using a hash object. If you have a user doc, pass in local as the hash object.

couchAuth.createSession(params)

Creates a new session for a user.

params has the properties:

  • login: username, email or UUID - if supported by your config
  • provider: the name of the provider. (eg. 'local', 'facebook', 'twitter'.)
  • sessionType: Optional. See security -> sessionConfig for details. Allows a dynamic session length by role.
  • byUUID: Optional. Allows to identify a user by UUID, even if login via UUID is not allowed in your config
couchAuth.changePassword(user_id, password)

Changes the user's password.

couchAuth.forgotPassword(email, req)

Sends out the forgot password email and issues a reset token.

couchAuth.resetPassword(form, req)

Resets the user's password. Required fields are token (from the forgot password email), password, and confirmPassword.

couchAuth.changeEmail(user_id, newEmail)

Changes the user's email. If email verification is enabled (local.sendConfirmEmail) then a new confirmation email will be sent out.

couchAuth.verifyEmail(token, req)

Marks the user's email as verified. token comes from the confirmation email.

couchAuth.addUserDB(user_id, dbName, type, designDoc, permissions, partitioned)

Associates a new database with the user's account. Will also authenticate all existing sessions with the new database.

  • dbName: the name of the database. For a shared db, this is the actual path. For a private db userDBs.privatePrefix will be prepended, and ${user_id} appended. (required)
  • type: 'private' (default) or 'shared' (optional)
  • designDoc: the name of the designDoc (if any) that will be seeded. (optional)
  • permissions: an array of permissions for use with Cloudant. (optional)
  • partitioned: false (default) or true if the database should be partitioned

If the optional fields are not specified they will be taken from userDBs.model.{dbName} or userDBs.model._default in your config.

couchAuth.removeUserDB(user_id, dbName, deletePrivate, deleteShared)

Deauthorizes the specified database from the user's account, and optionally destroys it.

  • dbName: the full path for a shared db, or the base name for a private db
  • deletePrivate: when true, will destroy a db if it is marked as private
  • deleteShared: when true, will destroy a db if it is marked as shared. Caution: may destroy other users' data!
couchAuth.logoutUser(user_id, session_id)

Logs out all of a user's sessions at once. If user_id is not specified CouchAuth will look it up from the session_id.

couchAuth.logoutSession(session_id)

Logs out the specified session.

couchAuth.logoutOthers(session_id)

Logs out all of a user's sessions, except for the one specified.

couchAuth.logoutAll(login, session_id)

Logs out all of a user's sessions. Retrieves the user by login or session_id

couchAuth.removeUser(user_id, destroyDBs)

Deletes a user, deauthorizes all the sessions, and optionally destroys all private databases if destroyDBs is true.

couchAuth.confirmSession(token, password)

Verifies a user's session.

couchAuth.removeExpiredKeys()

Deauthorizes every single expired session found in the user database.

couchAuth.sendEmail(templateName, email, locals)

Renders an email and sends it out. Server settings are specified under mailer in your config.

  • templateName: the name of a template object specified under emails in your config. See here for details.
  • email: the email address that the email
  • locals: local variables that will be passed into the nunjucks template to be rendered

couch-auth's People

Contributors

andyhasit avatar cgestes avatar colinsheppard10 avatar colinskow avatar dependabot[bot] avatar erikgoh avatar euandreh avatar fynnlyte avatar gregvis avatar ikosta avatar jkopcsek avatar jlcarvalho avatar karalix avatar klues avatar mahnuh avatar marcbachmann avatar marcmcintosh avatar maxcodefaster avatar micky2be avatar mugwhump avatar peteruithoven avatar pluscubed avatar ralphtheninja avatar skiqh avatar staxmanade avatar sukantgujar avatar tohagan avatar ybian avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

couch-auth's Issues

Thank you!

Hey @LyteFM

Just wanted to say thank you for providing this maintained fork of superlogin! I still love using PouchDB/CouchDB and was running into issues when upgrading to CouchDB v3. However, your fork solved all my issues, so once again, thank you!

Best
Christoph

Confirm email template not found

I was going through the tutorial on screen on getting started, I'm able to register and login but I'm unable to receive confirmEmail notification, I'm using a namecheap email to send out the emails.

I've tried adding a template for confirm email confirmEmail.njk

import express from "express";
import bodyParser from "body-parser";
import logger from "morgan";
import { SuperLogin } from "@perfood/couch-auth";

const app = express();
app.set("port", process.env.PORT || 3000);
app.use(logger("dev"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// Initialize SuperLogin
const superlogin = new SuperLogin({
  dbServer: {
    protocol: "http://",
    host: "localhost:5984",
    user: "admin",
    password: "passowrd",
    userDB: "sl-users",
    couchAuthDB: "_users",
  },
  mailer: {
    fromEmail: "[email protected]",
    options: {
      service: "email",
      auth: {
        user: "[email protected]",
        pass: "password",
      },
    },
  },
  userDBs: {
    defaultDBs: {
      private: ["supertest"],
    },
  },
});

// Mount SuperLogin's routes to our app
app.use("/auth", superlogin.router);
app.listen(app.get("port"));

But I get this error when I register a new user:
Screenshot 2022-04-21 at 00 25 37

Make username validation configurable or less restrictive

I've just migrated from superlogin to couch-auth, so first thanks for your work creating an up-to-date version of this framework!

Since the way usernames are stored and database names are chosen is differently in couch-auth (key value in user doc and <prefix><user-id> for database name), I think the validation of the username could be less restrictive:

  • user.ts#L102: I think there would be no problem if a username starts with _
  • util.ts#L13: I think there would be no problem in allowing usernames with uppercase letters or longer than 16 chars

So my proposal would be to just use a regex like /^[A-Za-z0-9_-]{2,50}$/ or to make it configurable.

Document update conflict upon login

Hey @fynnlyte,

I'm facing an issue where a user login fails with an internal server error. Upon inspecting the case I found that the /auth/login route repsonds with an error message which seems to be an upstream 409 error from CouchDB.

Here's the anonymised response I receive when trying to login with the given credentials (status 500):

{
  "scope": "couch",
  "statusCode": 409,
  "request": {
    "method": "post",
    "headers": {
      "content-type": "application/json",
      "accept": "application/json",
      "user-agent": "XXXXXX",
      "Accept-Encoding": "deflate, gzip"
    },
    "auth": {
      "username": "XXXXXX",
      "password": "XXXXXX"
    },
    "qsStringifyOptions": {
      "arrayFormat": "repeat"
    },
    "url": "http://localhost:5984/_users",
    "paramsSerializer": {},
    "data": "{\"_id\":\"org.couchdb.user:XXXXXX\",\"type\":\"user\",\"name\":\"XXXXXX\",\"user_uid\":\"XXXXXX\",\"user_id\":\"XXXXXX\",\"expires\":123456789,\"roles\":[\"XXXXXX\",],\"provider\":\"local\",\"password_scheme\":\"pbkdf2\",\"iterations\":10,\"salt\":\"XXXXXX\",\"derived_key\":\"XXXXXX\"}",
  },
  "errid": "non_200",
  "name": "Error",
  "description": "Document update conflict.",
  "error": "conflict",
  "reason": "Document update conflict."
}

I know the user tried to reset their password, which they said didn't work. I then reset their password to a default password using the changePassword method. This seemed to work (got no errors), but now it seems the internal state of this users doc is broken.

Any idea of this can be fixed? Thanks a lot for your help!

TypeError: CouchAuth is not a constructor

Hi, first of all thanks for this effort.

I followed the installation and it was installed successfully with no issues.
Following the simple minimalist configuration that will get me up and running right away resulted in an error.
I am using nodejs 16

var couchAuth = new CouchAuth(config);
^

TypeError: CouchAuth is not a constructor
at Object. (/home/mar/myapp/app.js:44:17)
at Module._compile (node:internal/modules/cjs/loader:1105:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
at node:internal/main/run_main_module:17:47

Re-introduce redis as optional session adapter

With 2d182fd, session caching in redis was removed in order to simplify the setup and make it compatible with Cloudant.

Since couch-auth is now primarily targeted at self-hosted CouchDB setups and we're transitioning more and more from โ€žoffline firstโ€œ to โ€žserver firstโ€œ -> add redis as optional dependency to improve performance of checking token validity.

Maybe use dynamic imports to achieve that?

Improve Documentation

  • Go through README and clean it up
    • e.g. -> should expose config...
  • Generate via typedoc and host on GH Pages, refer there from the Readme.

Add 'forgot-username' route and functionality

The users email address would be sent with the body and if that email address exists it would be sent an email with the username.
Currently if the user forgets their username there would be no way for them to ever gain access to their account.

Thanks for this brilliant updated fork of SuperLogin @LyteFM.

Automatically resolve session creation conflicts

If CouchDB or the express server crashes in the middle of processing a login request, a session token might already be generated but that token is not yet documented within sl-users. This leads to errors as:

Could not create session token with key: hgsi2BLWQleolfmw3JmscA - was inactiveSessions copied and does the key already exist?
Document update conflict.

Such conflicts should be resolved automatically. In case of a conflict error when creating the entry in _users:

  • retrieve the doc
  • verify that the UUID matches with the UUID of the user who is trying to log in
  • update that doc and resolve instead of throwing an error

โœจ Make session time more flexible

Currently, there is one fixed session time for every user.

It should be possible for the user to request a different, pre-configured session duration based on his request, e.g.:

  • 5 days if โ€žstay logged inโ€œ is selected
  • 5 minutes otherwise

The supported keys can be sent when logging in, resetting password - also when refreshing the session?

  • Or should the choice be persisted after logging in?
  • User could have multiple sessions: One with longer validity, one with shorter. Would be good to save the key (or duration?) directly at the session-record.
    • fall back to default if current config entry was dropped?
    • if existing config-entry was modified -> extend session life accordingly, not according to how it was initially set!

In a later iteration:

  • allow to specify a different session duration if users have a certain role assigned via the config -> shorter session life for admins/internal staff than for normal users

Having issue to retrieve user information after success user create

Hello,

I am having an issue, when i try to get the newly created user i am getting error 404
to repeat please follow the following steps

  1. Create a new user with createUser method
  2. after getting the response get the newly created user with getUser method

Package information
"@perfood/couch-auth": "^0.18.1",
"nano": "^10.0.0",

Error
Error: missing
at responseHandler (/Users/mohan/cibos-server/node_modules/nano/lib/nano.js:188:20)
at /Users/mohan/cibos-server/node_modules/nano/lib/nano.js:427:13
at processTicksAndRejections (node:internal/process/task_queues:96:5) {
scope: 'couch',
statusCode: 404,
request: {
method: 'get',
headers: {
'content-type': 'application/json',
accept: 'application/json',
'user-agent': 'nano/10.0.0 (Node.js v16.13.0)',
'Accept-Encoding': 'deflate, gzip'
},
auth: { username: 'XXXXXX', password: 'XXXXXX' },
qsStringifyOptions: { arrayFormat: 'repeat' },
url: 'http://localhost:5984/cibos-users/506c8f93227d42aa8c827ce7a766b1c4',
params: undefined,
paramsSerializer: [Function (anonymous)],
data: undefined,
maxRedirects: 0,
httpAgent: Agent {
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 80,
protocol: 'http:',
options: [Object: null prototype],
requests: [Object: null prototype] {},
sockets: [Object: null prototype],
freeSockets: [Object: null prototype],
keepAliveMsecs: 30000,
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 256,
scheduling: 'lifo',
maxTotalSockets: Infinity,
totalSocketCount: 3,
[Symbol(kCapture)]: false
},
httpsAgent: Agent {
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: [Object: null prototype],
requests: [Object: null prototype] {},
sockets: [Object: null prototype] {},
freeSockets: [Object: null prototype] {},
keepAliveMsecs: 30000,
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 256,
scheduling: 'lifo',
maxTotalSockets: Infinity,
totalSocketCount: 0,
maxCachedSessions: 100,
_sessionCache: [Object],
[Symbol(kCapture)]: false
}
},
headers: {
uri: 'http://localhost:5984/cibos-users/506c8f93227d42aa8c827ce7a766b1c4',
statusCode: 404,
'cache-control': 'must-revalidate',
'content-type': 'application/json',
date: 'Tue, 30 Aug 2022 11:16:35 GMT',
'x-couch-request-id': '495c245552',
'x-couchdb-body-time': '0'
},
errid: 'non_200',
description: 'missing',
error: 'not_found',
reason: 'missing'
}

Mailer option not working with nodemailer-ses-transport

Hello,

I was trying to integrate the nodemailer-ses-transport to send the email but it is throwing me the following error

image

my configuration is as follows
`
const sesTransporter = require('nodemailer-ses-transport');

mailer: {
fromEmail: '[email protected]',
transport: sesTransporter,
provider: 'ses',
options: {
accessKeyId: 'access key',
secretAccessKey: 'secret access key',
region: 'region',
bucket: 'bucket',
},
}
`

I am using node version 16.

Incorrect type for `SlLoginSession.profile`

Hi,

I think that in the typings the type of SlLoginSession.profile should be any instead of string. SlUserDoc.profile is of type any (which I think is correct), and SlLoginSession.profile is copied from SlUserDoc.profile in User.createSession(), so it should have the same type.

TypeError: Cannot read property 'user' of undefined

First, thanks for all your work on this library! I've been using it to get auth working quickly with couchdb.

Second, I'm seeing an error when attempting to register a new user with 0.14.0:

(node:79787) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'user' of undefined

To reproduce:
I think the relevant parts of my config are:

  local: {
    // Set this to true to disable usernames and use emails instead
    emailUsername: true,
  },

  userDBs: {
    defaultDBs: {
      private: ['user'],
      shared: []
    }
  }

Then, I issue the following request: The request succeeds with 200 OK

% curl -X POST \
  -d '{"email": "[email protected]", "password": "password1", "confirmPassword": "password1"}' \
  -H 'Content-Type: application/json' \
  http://localhost:3001/auth/register

{"success":"Request processed."}           

But looking at the server logs I see the following stacktrace:

POST /auth/register 200 26.512 ms - 32
(node:79787) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'user' of undefined
    at DBAuth.getDBConfig (/[path]/node_modules/@sl-nx/superlogin-next/lib/dbauth/index.js:199:101)
    at /Users/asullivan/Development/spock-express/node_modules/@sl-nx/superlogin-next/lib/user.js:1001:46
    at Array.forEach (<anonymous>)
    at processUserDBs (/[path]/node_modules/@sl-nx/superlogin-next/lib/user.js:1000:20)
    at SuperLogin.addUserDBs (/[path]/node_modules/@sl-nx/superlogin-next/lib/user.js:1018:9)
    at SuperLogin.insertNewUserDocument (/[path]/node_modules/@sl-nx/superlogin-next/lib/user.js:294:30)
    at /[path]/node_modules/@sl-nx/superlogin-next/lib/user.js:258:46
(node:79787) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:79787) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Do you know what this could be? I tried clearing everything in sl-users and _users but still no luck.

Make GET for email confirm token idempotent during validity

  • E-Mail providers might check the mail automatically before the user has a chance to click on them
  • User might have already clicked and wants to double check

Within the token validity, the route should just return OK and state that the E-Mail is successfully confirmed. This should be configurable via a config option and be turned off by default.

User doc _rev field not returned on user creation when loginOnRegistration is set to false

Hi, first all thanks for this updated library!

There is a bug I thing in the createUser() method. At the end, when the method resolves with the created user, the _rev field will only be included if it's returned by the second resolve() (i.e. if we create a user with loginOnRegistration set to false)

It can lead to CouchDB update conflicts, for instance if we try to update the user doc just after creation (as we will try to insert it without the _rev field).

// src/user.ts

// Let's say we have this.config.security.loginOnRegistration = false, and hasError = false.

return new Promise(async (resolve, reject) => {
  newUser = await this.prepareNewUser(newUser);
  if (hasError || !this.config.security.loginOnRegistration) {
    // 1. The promise resolves. It returns newUser instead of finalUser.
    resolve(hasError ? undefined : (newUser as SlUserDoc));
  }
  if (!hasError) {
    // 2. This is executed (even if the promise has already resolved).
    // The doc is inserted here, but resolve() has already been called so finalUser is not returned.
    const finalUser = await this.insertNewUserDocument(newUser, req);
    this.emitter.emit('signup', finalUser, 'local');
    if (this.config.security.loginOnRegistration) {
      resolve(finalUser);
    }
  }
});

Maybe we could change it like this?

  • I think that we can also remove the this.config.security.loginOnRegistration check, as we should return the created user no matter if it is set to true or false
  • We could also get rid of the Promise constructor
if(hasError)
  return;

else {
  newUser = await this.prepareNewUser(newUser);
  const finalUser = await this.insertNewUserDocument(newUser, req);
  this.emitter.emit('signup', finalUser, 'local');
  return finalUser;
}

PS: I'm using the latest version (0.16.0)

[question] Adding a provider - How to code a Passport strategy?

I am adding an Oauth provider. I was able to authenticate a user with this provider using oauth with postman. Now, I want to configure couch-auth to do it for me.

How do I create a Passport strategy? I am not using facebook, twitter, etc. I am using a very specific oauth provider. To authenticate a user, it expects a post to a specific URL with the following body:

{
    "client_id":"{app registration id}",
    "client_secret":"{app secret}",
    "username":"{username}",
    "password":"{provider's otp}",
    "grant_type":"password",
    "scope":"authentication_session"
}

Here, "grant_type":"password" means an "access_token" strategy.
I don't get where should I specify all these.

Handle case that user wants to change his email into one that already exists

Users occasionally want to change their email into an address with which they have already registered an account.

In that case, the email change requests indicates a success (because we don't allow account guessing) but nothing actually happens. The user is left in the dark.

Possible solution: Similar to the handling of the registration flow, send out an informational email that there is already an account associated with that email to the new email only. This way, the user can be informed that this account would need to be deleted before they can change into the mail.

Unhandled SMTP error when sending confirmation email in createUser()

Background

If createUser() in user.ts is called (in my case via the /register endpoint), and config.local.sendConfirmEmail is true, and your SMTP credentials are messed up (due to incorrect credentials, some other configuration error, creds being revoked, etc), nodeMailer will throw an exception which doesn't seem to be caught properly.

Actual Behavior

The promise returned by createUser() actually resolves successfully, the user is added to sl-users, and the /register route does call this line (routes.ts:153):
res.status(200).json({ success: 'Request processed.' });
But after this is executed, the async call to send the confirmation email runs, at insertNewUserDocument() (user.ts:475). My console prints this error

/usr/src/app/node_modules/@perfood/couch-auth/node_modules/nodemailer/lib/smtp-connection/index.js:787
            err = new Error(message);
                  ^

Error: Invalid login: 535 Authentication failed
    at SMTPConnection._formatError (/usr/src/app/node_modules/@perfood/couch-auth/node_modules/nodemailer/lib/smtp-connection/index.js:787:19)
    at SMTPConnection._actionAUTHComplete (/usr/src/app/node_modules/@perfood/couch-auth/node_modules/nodemailer/lib/smtp-connection/index.js:1539:34)
    at SMTPConnection.<anonymous> (/usr/src/app/node_modules/@perfood/couch-auth/node_modules/nodemailer/lib/smtp-connection/index.js:543:26)
    at SMTPConnection._processResponse (/usr/src/app/node_modules/@perfood/couch-auth/node_modules/nodemailer/lib/smtp-connection/index.js:950:20)
    at SMTPConnection._onData (/usr/src/app/node_modules/@perfood/couch-auth/node_modules/nodemailer/lib/smtp-connection/index.js:752:14)
    at TLSSocket.SMTPConnection._onSocketData (/usr/src/app/node_modules/@perfood/couch-auth/node_modules/nodemailer/lib/smtp-connection/index.js:191:44)
    at TLSSocket.emit (node:events:513:28)
    at addChunk (node:internal/streams/readable:315:12)
    at readableAddChunk (node:internal/streams/readable:289:9)
    at TLSSocket.Readable.push (node:internal/streams/readable:228:10) {
  code: 'EAUTH',
  response: '535 Authentication failed',
  responseCode: 535,
  command: 'AUTH PLAIN'
}

and express hangs since it's not handled. It doesn't matter if you set config.security.forwardErrors.

Expected behavior

The problematic code in the return statement of createUser():

    return new Promise(async (resolve, reject) => {
      newUser = await this.prepareNewUser(newUser);
      if (hasError || !this.config.security.loginOnRegistration) {
        resolve(hasError ? undefined : (newUser as SlUserDoc));
      }
      if (!hasError) {
        const finalUser = await this.insertNewUserDocument(newUser, req);
        this.emitter.emit('signup', finalUser, 'local');
        if (this.config.security.loginOnRegistration) {
          resolve(finalUser);
        }
      }
    });

It's "fixed", as in it doesn't hang express, if I wrap const finalUser = await this.insertNewUserDocument(newUser, req); in try/catch. But the async function inside the promise constructor looks like an instance of the Promise constructor anti-pattern. So you may want to rework that part.

I'm not sure if you want createUser() to reject when there's a problem sending the confirmation email; after all, the user WAS created, it's just that the confirmation email didn't go out. Basically createUser() in this case is a two-part non-atomic operation, so do you:

  1. Resolve createUser() if the user is created successfully, even if the confirmation email doesn't send? In this case I would prefer a different status be returned from /register, so that I can tell my users there was an issue sending the email. Then I'd either delete their user doc so they can try again, or add some page they can visit to re-send the confirmation email.
  2. Reject createUser() if the email doesn't send? In this case the newly-added doc should be deleted from sl-users.

2 is more atomic and less work for me. But people who don't have config.local.requireEmailConfirm might prefer approach 1.

requireAuth middleware deletes genuine couchdb users from _users when their username is in an Authorization: Bearer header

Background

I have a few special users in my _users db that weren't created through couch-auth. For example, special-purpose administrative users and a "public" user that can make simple requests to couch via Basic authentication without creating a session. I'd like them to stick around. But any endpoint protected by the requireAuth middleware can be used to delete these users. A basic example:

//endpoint expecting an Authorization: Bearer header validating a couch-auth user
router.get('/protected', couchAuth.requireAuth, function(req, res, next) {
  res.send('This endpoint is protected!');
})

Actual Behavior

A request of the form
curl -X GET http://localhost:3000/api/protected -H 'Authorization: Bearer public:madeUpPassword'
will delete the user named 'public' from _users.

Expected Behavior

The requireAuth middleware checks that the document in _users identified by the first part of the Bearer credentials is actually a session for a couch-auth user instead of a "normal" couchdb user, and only removes the former. Possibly through the presence of fields like 'user_uid', 'user_id', 'expires', etc.

Extract the core functionality into a separate package

Split out the core API s.t. Express doesn't need to be installed. Put that into a separate package.

  1. Extract everything that happens in user.ts + its dependencies (dbAuth, mailer,...) into a separate couch-auth-core module. Most of the programmatic API could be used without express.
  2. Add this module as dependency to couch-auth, import it in index.ts and routes.ts to get the current functionality.

"Request processed" on register but nothing in the db

Hi ! I have been testing SuperLogin over the weekend for an upcoming project but it seems I have reached a blocking point.

I am trying to make the code example from the README work with my instance of CouchDB on my distant server.

The /register POST seems OK

curl --request POST \
  --url http://localhost:3000/auth/register \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data '={
  "name": "Joe Smith",
  "username": "joesmith",
  "email": "[email protected]",
  "password": "bigsecret",
  "confirmPassword": "bigsecret"
}'

{
  "success": "Request processed."
}

but then there is nothing appearing in the Fauxton and of course the next step gives me some kind of error

curl --request POST \
  --url http://localhost:3000/auth/login \
  --header 'Authorization: Basic am9lc21pdGg6Ymlnc2VjcmV0'

{
  "message": "Missing credentials"
}

And the CLI doesn't help me much as there is no error message at all, only POST /auth/register 200 516.158 ms - 32

On the CouchDB logs, there is only this POST /sl-users/_find 200 ok 5 when I perform the request.

My SuperLogin config is as following :

{
  dbServer: {
    protocol: 'https://',
    host: 'krlx.ovh:6984',
    user: 'admin',
    password: 'redacted',
    userDB: 'sl-users',
    couchAuthDB: '_users'
  },
  mailer: {
    fromEmail: '[email protected]',
    options: {
      service: 'Gmail',
      auth: {
        user: '[email protected]',
        pass: 'redacted'
      }
    }
  },
  userDBs: {
    defaultDBs: {
      private: ['supertest']
    }
  }
}
CouchDB 3.1.1
SuperLogin 0.14.1
Node 14

Maybe you have an idea of what I did wrong ? Anyway thanks for maintaining this project.

Cloudant example

It works with couch but I couldn't make it work with cloudant. Is there an example?

This is incredible.

Thank you for sharing. This makes CouchDb usable! I was looking into how to add a REST "updateProfile" function so that users can update their display name, language, theme, etc...

I'll try and share what I get.

Unexpected behavior when email has uppercase letters

I was creating some test users when I found the following behavior:

  1. Create a user with an email that has an uppercase letter
        curl --request POST \
          --url http://localhost:3000/auth/register \
          --header 'Content-Type: application/json' \
          --data '{
          "email": "[email protected]",
          "password": "bigsecret",
          "confirmPassword": "bigsecret",
        }'
  2. Try to login
    curl --request POST \
      --url http://localhost:3000/auth/login \
      --header 'Content-Type: application/json' \
      --data '{
      "email": "[email protected]", 
      "password": "bigsecret"
    }
    '
  3. Receive an unauthorized error message
       {
         "error": "Unauthorized",
         "message": "Invalid username or password"
       }
  4. Change the email to all lowercase letters
    curl --request POST \
      --url http://localhost:3000/auth/login \
      --header 'Content-Type: application/json' \
      --data '{
      "email": "[email protected]", 
      "password": "bigsecret"
    }
    '
  5. Everything works normally
       {
         "issued": 1676830866748,
         "expires": 1676917266748,
         "provider": "local",
         "token": "NlKFvdkRSh-Z4Zf-z5QmbA",
         "password": "ZsO0x9N4Q6mUaOs_XHF7Jg",
         "user_id": "re46jmvr",
         "roles": [
           "user",
           "Admin",
           "user"
         ],
         "userDBs": {
           "interviewer": "http://NlKFvdkRSh-Z4Zf-z5QmbA:[email protected]:5985/interviewer_d31c511c86334591bcae45d9a5b6086e"
         }
       }

It took me a while to figure out why I was getting the Unauthorized message if I had just created the user, after checking that my configuration was correct (I disabled everything related to email confirmation) I had to start adding console.logs in the node_modules > couch-auth files

Finally realized the thing that was happening when I checked node_modules/@perfood/couch-auth/lib/user/DbManager.js that everything was correct but when querying the auth.email view it wasn't returning anything thats when I realized that the emails are saved in lower case.

I don't know if the login route should also lowercase the email or the create should save the email as is sent, my quick fix will be to lowercase the email before sending the login request as I have had some users report that they couldn't login even when the browser password manager saved their credentials.

PS:
I am currently using my fork https://github.com/ErikGoH/superlogin-next/tree/main-keycode but I don't think there are any significant changes
My package.json looks like this

...
"dependencies": {
   ...,
   "@perfood/couch-auth": "github:ErikGoH/superlogin-next#main-keycode",
   ...
}
My config
const config = {
      security: {
        loginOnRegistration: true,
      },
      testMode: {
        debugEmail: this.configService.get('TEST_MODE_DEBUG_EMAIL') === 'true',
        noEmail: this.configService.get('TEST_MODE_NO_EMAIL') === 'true',
      },
      dbServer: {
        protocol: this.configService.get<'http://' | 'https://'>('DB_PROTOCOL'),
        host: this.configService.get('DB_HOST'),
        user: this.configService.get('DB_USER'),
        password: this.configService.get('DB_PASSWORD'),
        userDB: this.configService.get('DB_USERDB_SL'),
        couchAuthDB: this.configService.get('DB_COUCH_AUTHDB'),
        publicURL: this.configService.get('DB_PUBLIC_URL'),
      },
      mailer: {
        fromEmail: this.configService.get('MAILER_FROMUSER'),
        transport: this.configService.get('MAILER_SENDGRID_APIKEY')
          ? nodemailerSendgrid
          : undefined,
        options: this.configService.get('MAILER_SENDGRID_APIKEY')
          ? {
              apiKey: this.configService.get(
                'MAILER_SENDGRID_APIKEY',
              ) as string,
            }
          : {
              host: this.configService.get('MAILER_HOST'),
              port: this.configService.get('MAILER_PORT'),
              secure:
                this.configService.get('MAILER_PORT') === '465' ? true : false,
              auth: {
                user: this.configService.get('MAILER_AUTHUSER'),
                pass: this.configService.get('MAILER_PASSWORD'),
              },
            },
      },
      local: {
        // Custom names for the username and password fields in your sign-in form
        usernameField: 'email',
        passwordField: 'password',
        emailUsername: true,
        usernameLogin: false,
        // Send out a confirm email after each user signs up with local login
        sendConfirmEmail: false,
        // Require the email be confirmed before the user can login  or before his changed email is updated
        requireEmailConfirm: false,
      },
      providers: {
        google: {
          credentials: {
            clientID: this.configService.get('GOOGLE_CLIENT_ID'),
            clientSecret: this.configService.get('GOOGLE_CLIENT_SECRET'),
            audience: [
              this.configService.get('GOOGLE_CLIENT_ID'),
              this.configService.get('GOOGLE_CLIENT_ID_CORDOVA'),
            ],
          },
          options: {
            scope: ['email'],
          },
          template: path.join(
             __dirname,
             './templates/oauth/my-custom-secure-auth-callback.ejs',
           ),
           templateTest: path.join(
             __dirname,
             './templates/oauth/my-custom-secure-auth-callback-test.ejs',
           ),
        },
        facebook: {
          credentials: {
            clientID: this.configService.get('FACEBOOK_APP_ID'),
            clientSecret: this.configService.get('FACEBOOK_APP_SECRET'),
            profileFields: ['id', 'displayName', 'name', 'emails'],
            fbGraphVersion: 'v3.2',
          },
          options: {
            scope: ['email', 'public_profile'],
          },
           template: path.join(
             __dirname,
             './templates/oauth/my-custom-secure-auth-callback.ejs',
           ),
        },
      },
      userDBs: {
        defaultDBs: {
          private: ['interviewer'],
        },
        model: {
          _default: {
            designDocs: [],
          },
          interviewer: {
            type: 'private',
            adminRoles: ['admin', 'AdminActive'],
            appendSeparator: this.configService.get('DB_APPEND_SEPARATOR'),
            designDocs: [], //'consumos', 'sesiones', 'seguimientos'
          },
        },
        designDocDir: path.join(__dirname, './ddocs'),
      },
      userModel: {
        whitelist: ['roles'],
        customValidators: {
          roleValidator: function (value: string[], validRoles: string[]) {
            if (!value || value.length < 1) {
              return 'El rol es requerido';
            }
            try {
              value.forEach((role) => {
                if (!validRoles.includes(role)) {
                  throw new Error(`: El rol no puede ser ${role}`);
                }
              });
            } catch (error) {
              type validatorError = { message?: string };
              const posibleError = error as validatorError;
              return posibleError?.message ?? 'Ocurrio un error';
            }
            return null;
          },
        },
        validate: {
          roles: {
            roleValidator: ['user', 'Interviewer', 'Admin'],
          },
        },
      },
    };

Refactor to use nano 9 instead of Cloudant

Use nano by default to reduce dependencies, but allow passing a cloudant - instance instead for IAM compatibility.

Or just discard cloudant completely now that it won't be supported anymore. It's still possible to use IBM Cloudant via legacy auth then.

Docs must be adjusted accordingly.

[Question] is there an option guard register route?

Hi, thanks for maintaining this library! I appreciate the effort to keep this library updated constantly.

I was just wondering if there is an option to guard the register route with logged in users? Because I have a private system which does not allow public users to register or login.

Fix issues with types

  • MailConfig probably isn't quite correct -> fixed.
  • UUID is now returned in some API responses
  • more for sure. Focus on the config and what's publically accessible, but internal typing should also be improved.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.