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.
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.
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.
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.
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
userobject that has all of the above fields populated. We'll use "populated" to refer to auserobject that has the appropriate set of fields for the context (for example, if being returned from the backend, thepasswordwill be missing or if being POSTed from the frontend, theidfield will be missing). We'll use "partially populated" to refer to auserobject 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.
NOTE: The user's password hash should never be returned from any of this endpoints. It should be stripped out from all
usersreturned in responses.
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
500andunknown-erroron server error.
Authorization: Anyone.
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.emailmust be unique. Returns400and'email-exists'error.user.email,user.name, anduser.passwordmust all be populated. Returns400andincomplete-usererror.user.passwordmust be at least 16 characters. Returns400andinadequate-passworderror.user.idis populated -- no error, just delete it.- Returns
500andunknown-erroron 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.
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.emailmust be unique (may match the user identified byidif provided). Returns400and'email-exists'error.user.email,user.name, anduser.passwordmust all be populated. Returns400andincomplete-usererror.user.emailmust be a valid email. Returns400andinvalid-emailerror.user.passwordmust be at least 16 characters. Returns400andinadequate-passworderror.- Returns
500andunknown-erroron 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.
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.emailmust be unique (may match the user identified byidif provided). Returns400and'email-exists'error.user.emailmust be a valid email. Returns400andinvalid-emailerror.user.passwordmust be at least 16 characters. Returns400andinadequate-passworderror.- Returns
500andunknown-erroron 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 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
idmust be provided. Returns400andno-userif noids are provided. - The provided
idnumbers must match existing users. Returns404if none of theids provided match existing users. If at least oneidmatches, then it will ignore the non-matches and move on. - Returns
500andunknown-erroron 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.
NOTE: The user's password hash should never be returned from any of this endpoints. It should be stripped out from all
usersreturned in responses.
Get the user identified by :id.
Request: No body.
Response: Populated user object matching :id, or 404 Not Found.
Errors:
:idmust match an existing user. Returns404andno-usererror.- Returns
500andunknown-erroron server error.
Authorization: Anyone.
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:
:idmust match an existing user. Returns404andno-usererror.user.emailmust be unique. Returns400and'email-exists'error.user.email,user.name, anduser.passwordmust all be populated. Returns400andincomplete-usererror.user.emailmust be a valid email. Returns400andinvalid-emailerror.user.passwordmust be at least 16 characters. Returns400andinadequate-passworderror.- Returns
500andunknown-erroron server error.
Response: The modified user object, populated.
Authorization: Anyone.
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:
:idmust match an existing user. Returns404andno-usererror.user.emailmust be unique. Returns400and'email-exists'error.user.email,user.name, anduser.passwordmust all be populated. Returns400andincomplete-usererror.user.emailmust be a valid email. Returns400andinvalid-emailerror.user.passwordmust be at least 16 characters. Returns400andinadequate-passworderror.- Returns
500andunknown-erroron 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.
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:
:idmust match an existing user. Returns404andno-usererror.user.emailmust be unique. Returns400and'email-exists'error.user.emailmust be a valid email. Returns400andinvalid-emailerror.user.passwordmust be at least 16 characters. Returns400andinadequate-passworderror.- Returns
500andunknown-erroron server error.
Authorization: Users must be logged in. Non-admin users may only modify themselves. Admin users may modify anyone.
Delete the user identified by :id.
Request: No body.
Action: Deletes the user identified by :id from the database.
Errors:
:idmust match an existing user. Returns404andno-usererror.- Returns
500andunknown-erroron 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.
We're also going to need to define the Authentication API.
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
500andunknown-erroron server error.
Authorization: Anyone.
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
400andmissing-emailerror if email isn't provided. - Returns
400andmissing-passworderror if password isn't provided. - Returns
403andauthentication-failederror if authentication failed. - Returns
500andunknown-erroron server error.
Response: The populated user object from the user session, representing the currently authenticated user.
Authorization: Anyone.
Destroy the currently authenticated user's session, logging them out.
Request: No body.
Action: Destroys the current user session.
Errors:
- Returns
500andunknown-erroron server error.
Response: Empty response.
Authorization: User must be logged in.
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.
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.
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.
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.
postUsers:
400,email-exists: Show "A user with that email already exists, please try logging in." underneath theemailfield.400,incomplete-user: Show "Please fill out all the fields." in theoverall-errorsection.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 thepassword-errorsection.500,unknown-error: Show "Something went wrong on our side. Please report a bug and try again." in theoverall-errorsection.
getAuthentication:
- 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
passwordandconfirmPasswordmatch. - Completion: validate that
name,password,email, andconfirmPasswordall have content before allowing submission.
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:
- User gets a link to
peer-review.io/confirm?token=aaa. - EmailConfirmationComponent takes
tokenand hitsGET /api/v/token/:tokenwhich returns404if the token doesn't exist, or200along withtypeanduserIdif it does exist.- On a successful token request, the backend goes ahead and starts a session for the user.
- If the request was successful, EmailConfirmationComponent confirms
typeequalsemail-confirmation.- When type is confirmed, the EmailConfirmationComponent submits a
GET /authenticationrequest to get the session, and then submits aPATCH /api/v/user/:idto update theemail_confirmedfield of the user totrue. - EmailConfirmationComponent returns user to the homepage.
- When type is confirmed, the EmailConfirmationComponent submits a
Possible reset password flow:
- User gets a link to
peer-review.io/reset?token=aaaa. - PasswordResetComponent takes
tokenand hitsGET /api/v/token/:token, this returns404if the token doesn't exist or200along withtypeanduserIdif it does.- If the token has expired it returns
<TBD error code>.
- If the token has expired it returns
- PasswordResetComponent, having confirmed the token is valid, presents the
user with a
passwordandpasswordConfirmationform, validates the input, and then sends aPATCH /user/:idrequest (where userId came from the token endpoint). WIP
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
400andtoken-expiredif that token has expired. - Returns
404if the token doesn't exist or if there's a type mismatch. - Returns
500andunknown-erroron server error.
Response: The populated user object matching the token or null.
Authorization: Anyone.
Break the work up into small, clearly scoped, releasable stories.