Skip to content

Instantly share code, notes, and snippets.

@simong
Last active December 17, 2015 08:58
Show Gist options
  • Select an option

  • Save simong/5583367 to your computer and use it in GitHub Desktop.

Select an option

Save simong/5583367 to your computer and use it in GitHub Desktop.
Password reset in OAE

Reset password pointers

General workflow

  1. The user 'Alice' forgets the password of her local account
  2. She clicks the 'Forgot my password'-link which takes her to /resetpassword
  3. A page with a form is displayed where she can enter the username she uses to login with (we currently do not have the functionality to look up a user by email)
  4. She fills in her username and submits the form
  5. The backend sends an e-mail to the e-mail address that is stored with the 'Alice' account
  6. There is a link in the e-mail that takes her to a page where she can enter a new password for the Alice account.

Couple of noteworthy things:

  • Submitting the form should not change the current password.
  • The generated link is only valid for x hours (24?)
  • The link contains a randomly generated string that will ensure it's a valid request.
  • Submitting the form doesn't actually take the user anywhere, but performs an AJAX request:
POST /api/auth/local/reset/init
username=alice
  • The link takes the user to
/resetpassword/alice/abcdefghijklmnopq

where abcdefghijklmnopq is the randomly generated string. (We'll need to add a rewrite entry in our nginx config.) That page shows the form where Alice can enter a new password, when she submits an AJAX request gets triggered:

POST /api/auth/local/reset/change
username=alice
secret=abcdefghijklmnopq
newPassword=<alice her new password>
  • The backend returns 200 if succesfull, 401 if not.

Some background information.

Each user can have multiple login IDs, this allows a user to login with multiple accounts to the same 'OAE account'. A user can for example, link his organization external SSO account, twitter account and Facebook account all to the same OAE account.

Some login ID examples:

  • cam:twitter:Alice1987 for logging in via the 'Alice1987' twitter account on the tenant with alias 'cam'
  • cam:local:alice for logging in with a local 'alice' account on the tenant with alias 'cam'

Data storage

There are a couple of column families (CF) in Cassandra (~tables in SQL world) that are of note

Principals

This contains the principal(=user or group) information. This CF has columns for:

  • the principalId, ex: u:cam:TTacvL3Hcf; The primary identifier for the user or group.
  • displayName, the user his or her name
  • locale, the user his or her locale
  • email, the user his or her email

AuthenticationUserLoginId

A simple row where the row key is the principalId identifying the user and a list of columns where each column is a loginId. Most rows will only have 1 loginId as most users will most likely only use one account to log in.

Say for example, that Alice has a local and a twitter loginId, her row might look something like this:

principalId | cam:local:alice | cam:twitter:Alice1987 u:cam:TTacvL3Hcf | 1 | 1

AuthenticationLoginId

This CF stores data against a login ID. In our usecase we're only interested in the local login strategy which stores the following data:

  • loginId - ex: cam:local:alice
  • password - A salted sha512 hash of the password

This is the CF will be storing the secret string in that allows Alice to reset her password.

The actual code

Most of the code in this area is located in the oae-authentication module. (We currently have a ticket to clean up the API and split it up in multiple files, so it's not the prettiest right now)

What will be of interest to you:

oae-authentication/lib/rest.js

This file contains all the authentication-related REST endpoints. We try to keep our rest.js files as clean as possible, so we usually call down to the proper api functions.

Well need to add 2 endpoints: /api/auth/local/reset/init calls AuthenticationAPI.getResetPasswordSecret(req.ctx, req.body.username, function(err) { .. }); /api/auth/local/reset/change calls AuthenticationAPI.resetPassword(req.ctx, req.body.username, req.body.secret, req.body.newPassword, function(err) { .. })

oae-authentication/lib/api.js

This file contains all the authentication logic and is the place where we'll add the logic for resetting the password.

Init phase

The getResetPasswordSecret will do the following things:

  • Construct the cam:local:alice loginId
        var loginId = new LoginId(ctx.tenant().alias, AuthenticationConstants.providers.LOCAL, username);
  • Check if that login ID is stored in Cassandra (ie: that it's associated to some account)
        _getUserIdFromLoginId(loginId, function(err, userId) { .. })
  • Get the User object as we'll need it to send Alice an e-mail.
        PrincipalsAPI.getUser(ctx, userId, function(err, user) { .. })
  • Generate a random string [1] and store it in the AuthenticationLoginId CF in the secret column. We can set a TTL of 24 hours on the secret column, so it automatically expires.
        var secret = crypto.randomBytes(16).toString('hex');
        Cassandra.runQuery('UPDATE AuthenticationLoginId USING CONSISTENCY QUORUM AND TTL 86400 SET secret = ? WHERE loginId = ?', [secret, _flattenLoginId(loginId)], function(err) { .. })
  • Send out an e-mail which we can do with the oae-email module [2].
        var emailData = {'secret': secret};
        EmailsAPI.sendEmail('oae-authentication', 'reset-password', user, emailData, null, function(err) { .. })
  • We're done, pass control back to the REST endpoint which can send back a 200.
        callback();

In order for the above to work you'll need to add an email template to the oae-authentication module. Have a look at the oae-content/emailTemplates to see how they work

Change password phase

The resetPassword will do the following things:

  • Construct the cam:local:alice loginId
        var loginId = new LoginId(ctx.tenant().alias, AuthenticationConstants.providers.LOCAL, username);
  • Get the secret from the database
        Cassandra.runQuery('SELECT userId, secret FROM AuthenticationLoginId USING CONSISTENCY QUORUM WHERE loginId = ?', [_flattenLoginId(loginId)], function(err, rows) {
            if (err) { return callback(err); }

            // Get the secret column out of the row
            var dbSecret = rows[0].get('secret')

            // If the secret column was not found or its value didn't match, something is wrong.
            if (!dbSecret || dbSecret.value !== secret) {
                log().warn({'loginId': loginId}, 'Potential hacking attempt detected');
                return callback({'code': 401, 'msg': 'The secret was not found or incorrect'})
            }
  • Change the password.
            _changePassword(loginId, newPassword, callback);
  • Done.

UI Pointers

nginx.conf

You'll need to add 2 entries to your nginx config rewrite ^/resetpassword$ /ui/resetpassword.html last; rewrite ^/resetpassword/(.*) /ui/resetpassword.html last;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment