Skip to content

Commit

Permalink
Merge pull request #4 from sgithens/GPII-3255
Browse files Browse the repository at this point in the history
GPII-3255
  • Loading branch information
the-t-in-rtf authored Nov 6, 2018
2 parents 5032878 + 1ac5fdd commit 010bbaf
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 67 deletions.
62 changes: 62 additions & 0 deletions docs/utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Utility Components and Methods API

A number of useful routines for user management are available via a promise based API.
These are useful for writing management and batch routines, building other HTTP interfaces
and services on top of the system, and testing.

## `gpii.express.user.utils`

Component containing a number of useful utility methods. Detailed JSDocs for the
component and invokers are located in [utils.js](../src/js/server/utils.js).

### Component Options

This component requires the same CouchDB options as other components in this module.

| Option | Type | Description |
| ------------------ | ---------- | ----------- |
| `couch.port` | `{String}` | The port on which our CouchDB instance runs. |
| `couch.userDbName` | `{String}` | The CouchDB database name containing our user records. Defaults to `users`. |
| `couch.userDbUrl` | `{String}` | The URL of our CouchDB instance. By default this is expanded from `userDbName` above using the pattern `http://admin:admin@localhost:%port/%userDbName` (see above). |

### Component Invokers

### `{that}.createNewUser(userData)`

* `userData` - Take an object with `username`, `email`, and `password` entries.
* Returns: A promise resolving with the new CouchDB user record on success, or
a promise rejecting with an `error` property and message on failure.

Creates a new user in the system with a username, email, and password.

```javascript
utils.createNewUser({username: "alice", email: "[email protected]", password: "#1 Cloudsafe!"});
```

### `{that}.verifyPassword(userRecord, password)`

* `userRecord` - Takes a full user record object as stored in CouchDB and checks to see if the
supplied password matches. In general this should be a record retrieved from CouchDB and not
something you generate yourself. For reference, the `userRecord` structure can be seen in
this [test fixture](https://github.com/GPII/gpii-express-user/blob/a83beacdffb4096a379fe91a8f6e23979839fdd8/tests/data/users.json).
* `password` - Password to verify against the userRecord.
* Returns: `true` if the password matches, otherwise `false`.

```javascript
var userRecord = couchDBStore.getUserRecordFromCouchForUsernameOrEmail("alice");
utils.verifyPassword(userRecord, "#1 CloudSafe!"); // true if this is their password
```

Returns a promise that will contain the user recored on a successful resolve.

### `{that}.unlockUser(username, password)`

* `username` - Username string to unlock.
* `password` - Password string to use for unlocking `username`.
* Returns a promise resolving with the `userData` record if the password is correct,
otherwise rejecting with an `isError` Object.

```javascript
var userRecord = utils.unlockUser("alice", "#1 Cloudsafe!");
// userRecord is their full internal record from CouchDB
```
9 changes: 9 additions & 0 deletions src/js/server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require("./logout.js");
require("./reset.js");
require("./signup.js");
require("./verify.js");
require("./utils.js");

fluid.registerNamespace("gpii.express.user.api");

Expand Down Expand Up @@ -55,6 +56,10 @@ fluid.defaults("gpii.express.user.api", {
"source": "{that}.options.couch",
"target": "{that gpii.express.router}.options.couch"
},
{
"source": "{that}.options.couch",
"target": "{that gpii.express.user.utils}.options.couch"
},
{
source: "{that}.options.app",
target: "{that gpii.express.router}.options.app"
Expand All @@ -65,6 +70,9 @@ fluid.defaults("gpii.express.user.api", {
}
],
components: {
utils: {
type: "gpii.express.user.utils"
},
// API Endpoints (routers)
current: {
type: "gpii.express.user.current",
Expand Down Expand Up @@ -132,6 +140,7 @@ fluid.defaults("gpii.express.user.api", {
}
});


/*
A mix-in grade that adds the required session middleware to an instance of `gpii.express` or `gpii.express.router`.
Expand Down
26 changes: 9 additions & 17 deletions src/js/server/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,17 @@ require("./lib/password");

fluid.registerNamespace("gpii.express.user.login");

gpii.express.user.login.post.handler.verifyPassword = function (that, response) {
// The user exists, so we can check the supplied password against our records.
if (response.username) {
var encodedPassword = gpii.express.user.password.encode(that.options.request.body.password, response.salt, response.iterations, response.keyLength, response.digest);
if (encodedPassword === response.derived_key) {
// Transform the raw response to ensure that nothing sensitive is exposed to the user
var user = fluid.model.transformWithRules(response, that.options.rules.user);
gpii.express.user.login.post.handler.verifyPassword = function (that, utils, request) {
utils.unlockUser(request.body.username, request.body.password).then(
function (data) {
var user = fluid.model.transformWithRules(data, that.options.rules.user);
that.options.request.session[that.options.sessionKey] = user;
that.sendResponse(200, { message: that.options.messages.success, user: user});
}
// The password didn't match.
else {
},
function () {
that.sendResponse(401, { isError: true, message: that.options.messages.failure});
}
}
// The user doesn't exist, but we send the same failure message to avoid giving intruders a way to validate usernames.
else {
that.sendResponse(401, { isError: true, message: that.options.messages.failure});
}
);
};

fluid.defaults("gpii.express.user.login.post.handler", {
Expand All @@ -50,8 +42,8 @@ fluid.defaults("gpii.express.user.login.post.handler", {
method: "post",
invokers: {
handleRequest: {
func: "{reader}.get",
args: ["{that}.options.request.body"]
funcName: "gpii.express.user.login.post.handler.verifyPassword",
args: ["{that}", "{gpii.express.user.utils}", "{that}.options.request"]
}
},
components: {
Expand Down
62 changes: 12 additions & 50 deletions src/js/server/signup.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/* eslint-env node */
"use strict";

var fluid = require("infusion");
var gpii = fluid.registerNamespace("gpii");

var request = require("request"); // TODO: Replace this with a writable data source.

require("gpii-handlebars");

require("./lib/datasource");
Expand All @@ -19,48 +18,22 @@ gpii.express.user.signup.post.handler.lookupExistingUser = function (that) {
that.reader.get(that.options.request.body).then(that.checkForExistingUser);
};

gpii.express.user.signup.post.handler.checkForExistingUser = function (that, response) {
gpii.express.user.signup.post.handler.checkForExistingUser = function (that, utils, response) {
if (response && response.username) {
that.sendResponse(403, { isError: true, message: "A user with this email or username already exists."});
}
else {
// Encode the user's password
var salt = gpii.express.user.password.generateSalt(that.options.saltLength);
var derived_key = gpii.express.user.password.encode(that.options.request.body.password, salt);
var code = gpii.express.user.password.generateSalt(that.options.verifyCodeLength);

// Our rules will set the defaults and pull approved values from the original submission.
var combinedRecord = fluid.model.transformWithRules(that.options.request.body, that.options.rules.write);

// Set the "name" to the username for backward compatibility with CouchDB
combinedRecord.salt = salt;
combinedRecord.derived_key = derived_key;
combinedRecord[that.options.codeKey] = code;

// Set the ID to match the CouchDB conventions, for backward compatibility
combinedRecord._id = "org.couch.db.user:" + combinedRecord.username;

// Save the record for later use in rendering the outgoing email
that.user = combinedRecord;

// Write the record to couch. TODO: Migrate this to a writable dataSource.
var writeOptions = {
url: that.options.urls.write,
method: "POST",
json: true,
body: combinedRecord
};
request(writeOptions, function (error, response, body) {
if (error) {
that.sendResponse(500, {isError: true, message: error});
}
else if ([200, 201].indexOf(response.statusCode) === -1) {
that.sendResponse(response.statusCode, { isError: true, message: body});
}
else {
var body = that.options.request.body;
utils.createNewUser(body).then(
function (data) {
// Save the record for later use in rendering the outgoing email
that.user = data;
that.sendMessage();
},
function (error) {
that.sendResponse(500, {isError: true, message: error});
}
});
);
}
};

Expand All @@ -77,17 +50,6 @@ fluid.defaults("gpii.express.user.signup.post.handler", {
error: "A verification code could not be sent. Contact an administrator."
},
rules: {
// Only let the user supply a very particular set of fields.
write: {
"name": "username", // Default rules are designed to cater to CouchDB and express-couchUser conventions, but can be overriden.
"username": "username",
"email": "email",
roles: { literalValue: []},
type: { literalValue: "user"},
password_scheme: { literalValue: "pbkdf2"},
iterations: { literalValue: 10},
verified: { literalValue: false}
},
mailOptions: {
to: "user.email",
subject: { literalValue: "Please verify your account..."}
Expand All @@ -108,7 +70,7 @@ fluid.defaults("gpii.express.user.signup.post.handler", {
},
"checkForExistingUser": {
funcName: "gpii.express.user.signup.post.handler.checkForExistingUser",
args: ["{that}", "{arguments}.0"]
args: ["{that}", "{gpii.express.user.utils}", "{arguments}.0"]
}
},
components: {
Expand Down
Loading

0 comments on commit 010bbaf

Please sign in to comment.