Skip to content

Latest commit

 

History

History
601 lines (380 loc) · 20.9 KB

File metadata and controls

601 lines (380 loc) · 20.9 KB

Authentication

As a user, I want to be able to create an account and login because it will allow me to have ownership of my papers, comments, and votes.

As a developer, I want to use an authentication and authorization system to ensure users may only perform the actions they are allowed to.

Background and Description

On a high level, what do we want to achieve? Why are we doing it and what related things have we done before?

We need to create the authentication and authorization system so that we can allow users to create accounts and login. This will allow them to post paper drafts, to give reviews, to accept reviews and publish their papers, to post paper responses, and to vote on papers. It will allow us to ensure users can only perform the actions they are allowed to.

This is generally one of the first things we need to create when starting a new web app.

Acceptance Criteria

How will we know when the milestone has been achieved? These should be verifiable, testable statements.

  • Users can register themselves with a name, email, and password.
    • Users are asked to confirm their password and rejected if the passwords don't match.
    • Emails are validated and rejected if they are not valid emails.
    • Attempts to register are rejected if a user with that email already exists.
    • Passwords are validated and rejected if they aren't strong enough (based solely on length).
    • Users are sent an email with a link to a token endpoint to confirm their email.
  • Registered users can login using their email and password.
    • Registered users can request a password reset, and are sent a link with a token to allow them to reset.
  • Users can view their profile.
    • Users viewing their own profile are shown an edit button.
  • Users can edit their profile.
    • Users can change their name.
    • Users must confirm their password to change their email or password.
    • Users changing their email must verify their email ownership by being sent a token.
  • Users are presented with links to login and register in the main navigation when not logged in.
  • Users are presented with links to view their profile and logout in the main navigation when logged in.
  • Users who are logged in may log out.

What do we need to do?

Here's where you can brainstorm and document what we need to do to achieve this milestone and how we can go about doing it.

Users API

First, we'll need to define the Users API.

We'll start by defining the user object:

{
    id: integer, // The unique identifier of the user.
    name: string, // The user's name.
    email: string, // The user's email, used as a login credential.
    password: string // The user's password, used as a login credential.  Never sent to the frontend.
}

NOTE: We'll use "fully populated" to refer to a user object that has all of the above fields populated. We'll use "populated" to refer to a user object that has the appropriate set of fields for the context (for example, if being returned from the backend, the password will be missing or if being POSTed from the frontend, the id field will be missing). We'll use "partially populated" to refer to a user object missing one or more fields.

Then we need to define the end points. We'll use the verbs GET, POST, PUT, PATCH, and DELETE with both the singular and plural endpoints. Unless otherwise noted, the request body and return will be json.

/users Endpoint

NOTE: The user's password hash should never be returned from any of this endpoints. It should be stripped out from all users returned in responses.

GET /users:

Retrieve all users.

Request: No body.

Response: All users in the database as an array containing populated user objects, or an empty array if there are no users.

Errors:

  • Returns 500 and unknown-error on server error.

Authorization: Anyone.

POST /users:

Create one or more users.

Request: One or more populated user objects in an array. The id field may not be populated, and will be ignored if provided.

Action: Adds the posted users to the database as new users. They will be assigned autogenerated id numbers. We also need to validate that their emails exist, by sending an email with a token. Use EmailValidationService to validate the email (see below).

Errors:

  • user.email must be unique. Returns 400 and 'email-exists' error.
  • user.email, user.name, and user.password must all be populated. Returns 400 and incomplete-user error.
  • user.password must be at least 16 characters. Returns 400 and inadequate-password error.
  • user.id is populated -- no error, just delete it.
  • Returns 500 and unknown-error on server error.

Response: The newly added user(s) with id populated, as an array containing user objects.

Authorization: If no user is logged in, then the request is limited to a single user. Admin users may submit multiple users.

PUT /users:

Create or overwrite one or more users.

Request: One or more populated user objects in an array. The id field may be populated.

Action: Adds the users in the request body to the database. If the id field is populated, will replace users on that id. Otherwise, creates new users. If the email is different, then use the EmailValidationService to validate the email (see below).

Errors:

  • user.email must be unique (may match the user identified by id if provided). Returns 400 and 'email-exists' error.
  • user.email, user.name, and user.password must all be populated. Returns 400 and incomplete-user error.
  • user.email must be a valid email. Returns 400 and invalid-email error.
  • user.password must be at least 16 characters. Returns 400 and inadequate-password error.
  • Returns 500 and unknown-error on server error.

Response: The newly added user(s) with id populated, as an array containing user objects.

Authorization: User must be logged in. Admin users may submit an arbitrary number of users. Other users may only submit a single user, and it must be themselves.

PATCH /users:

Update one or more users with a partial set of fields.

Request: One or more partially populated user objects in an array. The id field must be populated. If the email

Action: Merges the fields in the provided user objects with those populated in the database, overwriting the database. If the email is different, then use the EmailValidationService to validate the email (see below).

Errors:

  • user.email must be unique (may match the user identified by id if provided). Returns 400 and 'email-exists' error.
  • user.email must be a valid email. Returns 400 and invalid-email error.
  • user.password must be at least 16 characters. Returns 400 and inadequate-password error.
  • Returns 500 and unknown-error on server error.

Response: The modified user objects, fully populated, in an array, or an empty array if none were modified.

Authorization: Users must be logged in. Non-admin users may only submit a single user, and it must be themselves. Admin users may submit an arbitrary numbers of users.

DELETE /users:

Delete one or more users.

Request: One or more partially populated user objects in an array. The id field must be populated and is the only field that is required. The other fields are ignored.

Action: Deletes the provided users from the database.

Errors:

  • At least one id must be provided. Returns 400 and no-user if no ids are provided.
  • The provided id numbers must match existing users. Returns 404 if none of the ids provided match existing users. If at least one id matches, then it will ignore the non-matches and move on.
  • Returns 500 and unknown-error on unknown server error.

Response: Returns an array containing the id numbers of the deleted users, or an empty array.

Authorization: User must be logged in. Non-admin users may only submit a single user and it must be themselves. Admin users may submit an arbitrary number of users.

/user/:id Endpoint

NOTE: The user's password hash should never be returned from any of this endpoints. It should be stripped out from all users returned in responses.

GET /user/:id:

Get the user identified by :id.

Request: No body.

Response: Populated user object matching :id, or 404 Not Found.

Errors:

  • :id must match an existing user. Returns 404 and no-user error.
  • Returns 500 and unknown-error on server error.

Authorization: Anyone.

POST /user/:id:

Overwrite the user identified by :id.

Request: A populated user object.

Action: Synonym for PUT /user/:id. Overwrites the user in the database identified by :id with the provided user object.

Errors:

  • :id must match an existing user. Returns 404 and no-user error.
  • user.email must be unique. Returns 400 and 'email-exists' error.
  • user.email, user.name, and user.password must all be populated. Returns 400 and incomplete-user error.
  • user.email must be a valid email. Returns 400 and invalid-email error.
  • user.password must be at least 16 characters. Returns 400 and inadequate-password error.
  • Returns 500 and unknown-error on server error.

Response: The modified user object, populated.

Authorization: Anyone.

PUT /user/:id:

Overwrite the user identified by :id.

Request: A populated user object.

Action: Synonym for POST /user/:id. Overwrites the user identified by :id with the user object provided.

Errors:

  • :id must match an existing user. Returns 404 and no-user error.
  • user.email must be unique. Returns 400 and 'email-exists' error.
  • user.email, user.name, and user.password must all be populated. Returns 400 and incomplete-user error.
  • user.email must be a valid email. Returns 400 and invalid-email error.
  • user.password must be at least 16 characters. Returns 400 and inadequate-password error.
  • Returns 500 and unknown-error on server error.

Response: The modified user object, populated.

Authorization: User must be logged in. Non-admin users may only modify themselves. Admin users may modify any user.

PATCH /user/:id:

Update the user identified by :id.

Request: A partially populated user object in an array.

Action: Merges the fields in the provided user object with those populated in the database as identified by :id, overwriting the database.

Response: The modified user object, populated.

Errors:

  • :id must match an existing user. Returns 404 and no-user error.
  • user.email must be unique. Returns 400 and 'email-exists' error.
  • user.email must be a valid email. Returns 400 and invalid-email error.
  • user.password must be at least 16 characters. Returns 400 and inadequate-password error.
  • Returns 500 and unknown-error on server error.

Authorization: Users must be logged in. Non-admin users may only modify themselves. Admin users may modify anyone.

DELETE /user/:id:

Delete the user identified by :id.

Request: No body.

Action: Deletes the user identified by :id from the database.

Errors:

  • :id must match an existing user. Returns 404 and no-user error.
  • Returns 500 and unknown-error on server error.

Response: An object containing only the id number of the user deleted.

Authorization: User must be logged in. Non-admin users may only delete themselves. Admin users may delete any user.

Authentication API

We're also going to need to define the Authentication API.

'/authentication` Endpoint

GET /authentication:

Retrieve the currently authenticated user, if any.

Request: No body.

Action: No action.

Response: Populated user object from the user session, representing the currently authenticated user or 204 if no currently authenticated user.

Errors:

  • Returns 500 and unknown-error on server error.

Authorization: Anyone.

POST /authentication:

Authenticate a user, logging them in.

Request: An object containing the user's credentials (email and password).

Action: Authenticates the user against the database, and on successful authentication, creates the session and populates it with the user object for the user.

Errors:

  • Returns 400 and missing-email error if email isn't provided.
  • Returns 400 and missing-password error if password isn't provided.
  • Returns 403 and authentication-failed error if authentication failed.
  • Returns 500 and unknown-error on server error.

Response: The populated user object from the user session, representing the currently authenticated user.

Authorization: Anyone.

DELETE /authentication:

Destroy the currently authenticated user's session, logging them out.

Request: No body.

Action: Destroys the current user session.

Errors:

  • Returns 500 and unknown-error on server error.

Response: Empty response.

Authorization: User must be logged in.

Users Redux Slice

Create a redux slice for the Users API with thunks for each of the endpoints. The initial state of the slice should contain a dictionary for users retrieved from the backend and a dictionary for requests we have made or are making to the backend.

{
    requests: {},
    users: {}
}

Create generic makeRequest, failRequest, completeRequest, and cleanupRequest methods. These methods will work primarily with the requests hash to track requests in progress. We want to be able to track more than one request in process, because its plausible that we would kick off several requests before the first one returns and we want to make sure we're not in a situation where we're losing track of requests.

completeRequest will assume we're getting one or more populated user object(s) and adding them to the hash. If we need to do something different at the end of a request, such as delete a user from the dictionary, then we'll need a specific complete method of the form complete[Method][EndpointBase]Request. For example, completeDeleteUserRequest.

In this case, we only need a completeDeleteUserRequest, all other methods should return either an array of user objects or a single user object.

Authentication Redux Slice

Create an authentication slice for the Authentication API with thunks for the three authentication endpoints. The initial state of the slice shoudl contain a single currentUser object, representing the currently logged in user, and a dictionary of requests in progress.

{
    requests: {},
    currentUser: null
}

Create generic makeRequest, failRequest, completeRequest, and cleanupRequest methods. completeRequest will assume we're recieving a single populated user object to set as the currentUser.

We shouldn't need any specific complete methods, since each endpoint will either return a user object or null, which will have the effect of either setting the currentUser or unsetting them.

TODO: Authentication is probably one place where we only want a single request running at a time. After all there's only a single session to retrieve, and if we're already retrieving it, then we don't need to kick off another request for it.

AuthenticationNavigation Component

We'll need a component to live in the navigation header and present navigation links for authentication. When there is no current user, those navigation links will be login and register. When there is a currentUser, they will be a link to the user profile (/user/:id/) displaying their username, and a logout link.

The component will need to query the getAuthentication thunk to determine if there is a current user session. If there is, show that view, otherwise show the default view. It needs to store a record of having made that request, so that it doesn't remake it on every load and end up render looping.

RegistrationForm Component

Create a RegistrationForm component to allow users to register themselves on the site. It needs to collect Name, Email, Password, and it should have a PasswordConfirmation field to ensure the user typed the password correctly.

It should validate the email and password, and that the password and password confirmation match, on the client side before calling the postUsers thunk to create a new user.

It should record the requestId for the postUsers request. When that request completes, if it completes successfully, it should use the same information to call the getAuthentication endpoint and create the session.

Error Handling

postUsers:

  • 400, email-exists: Show "A user with that email already exists, please try logging in." underneath the email field.
  • 400, incomplete-user: Show "Please fill out all the fields." in the overall-error section.
  • 400, inadequate-password: Show "Please select a strong password, of at least 16 characters in length. We recommend the XKCD method of choosing passwords if you don't have a password manager to generate and store a random one." in the password-error section.
  • 500, unknown-error: Show "Something went wrong on our side. Please report a bug and try again." in the overall-error section.

getAuthentication:

Validation

  • Email: Keep it simple, just validate the precense of an @, we'll validate that the email actually exists on the backend.GET /authentication
  • Password: validate that the password is at least 16 characters long. Recommend the xkcd method.
  • Confirm Password: validate that password and confirmPassword match.
  • Completion: validate that name, password, email, and confirmPassword all have content before allowing submission.

LoginForm Component

Tokens

Authentication is going to require at least two flows that involve a landing page with a token in the query string: the reset password flow and the and the confirm email flow.

In the former flow, after we've confirmed the token, we'll want to show a form allowing the user to reset their password.

In the later flow, all we need to do is confirm their token, and then we can redirect them to the home page with their email confirmed.

In both cases, although there is technically a resource here (password reset in the first, email confirmation in the second), there isn't really much to build a rest API around.

In password reset, we just need to confirm that the reset token is valid. Then we'll collect the new password on the front end and probably send a PATCH /user/:id request to update it on the backend.

In email confirmation, we just need to confirm that the confirmation token is valid.

Possible email confirmation flow:

  1. User gets a link to peer-review.io/confirm?token=aaa.
  2. EmailConfirmationComponent takes token and hits GET /api/v/token/:token which returns 404 if the token doesn't exist, or 200 along with type and userId if it does exist.
    1. On a successful token request, the backend goes ahead and starts a session for the user.
  3. If the request was successful, EmailConfirmationComponent confirms type equals email-confirmation.
    1. When type is confirmed, the EmailConfirmationComponent submits a GET /authentication request to get the session, and then submits a PATCH /api/v/user/:id to update the email_confirmed field of the user to true.
    2. EmailConfirmationComponent returns user to the homepage.

Possible reset password flow:

  1. User gets a link to peer-review.io/reset?token=aaaa.
  2. PasswordResetComponent takes token and hits GET /api/v/token/:token, this returns 404 if the token doesn't exist or 200 along with type and userId if it does.
    1. If the token has expired it returns <TBD error code>.
  3. PasswordResetComponent, having confirmed the token is valid, presents the user with a password and passwordConfirmation form, validates the input, and then sends a PATCH /user/:id request (where userId came from the token endpoint). WIP

Token API

GET /token/:token

Determine whether a token is valid.

Request: An object containing the type of token we're confirming.

Action: Confirms the token exists in the database, the type on the request matches the type in the database, and hasn't expired. Creates a session for the user matching the token.

Errors:

  • Returns 400 and token-expired if that token has expired.
  • Returns 404 if the token doesn't exist or if there's a type mismatch.
  • Returns 500 and unknown-error on server error.

Response: The populated user object matching the token or null.

Authorization: Anyone.

How should we break up the work?

Break the work up into small, clearly scoped, releasable stories.