3.0.1 • Published 2 years ago

@apparts/login-server v3.0.1

Weekly downloads
46
License
MIT
Repository
github
Last release
2 years ago

#+TITLE: Login and Signup #+DATE: 2019-02-06 Wed #+AUTHOR: Philipp Uhl

  • Backend

** Usage

  1. ~npm i --save @apparts/login-server~
  2. Add the package to your codebase: #+BEGIN_SRC js const { addRoutes, createUseUser } = require("@apparts/login-server"); const express = require("express");

    const { useUser } = createUseUser();

    const app = express(); // ...

    const mail = { async sendMail(to, body, title){ // ... } }; addRoutes(app, useUser, mail); // ... #+END_SRC

    • If you need to specify the route version by yourself: #+BEGIN_SRC js const newApiVersion = 2; addRoutes(app, useUser, mail, newApiVersion); // routes will start with /v/2/...

      const oldApiVersion = 1; addRoutes.upgrade(app, oldApiVersion, (_, res) => { res.status(410); res.send("Your app is too old. Please upgrade your app to the newest version."); }); // all /v/1/... routes will send 410 #+END_SRC

    • If you need to specify the route names (and methods) by yourself: #+BEGIN_SRC js const { routes } = require("@apparts/login-server");

      const { addUser, getUser, getToken, getAPIToken, deleteUser, updateUser, resetPassword, } = routes(useUser, mail);

      // E.g.: app.post("/v/1/user", addUser); app.get("/v/1/user/login", getToken); app.get("/v/1/user/apiToken", getAPIToken); app.get("/v/1/user", getUser); app.delete("/v/1/user", deleteUser); app.put("/v/1/user", updateUser); app.post("/v/1/user/:email/reset", resetPassword); #+END_SRC

    • If you need to configure the User-model:

      • The table name (default ~users~): #+BEGIN_SRC js const { createUseUser } = require("@apparts/login-server"); const { useUser } = createUseUser(undefined, "myTableName"); #+END_SRC
      • The types of the user (see [https://github.com/phuhl/apparts-types#usage]): #+BEGIN_SRC js const { createUseUser } = require("@apparts/login-server"); const { useUser } = createUseUser({ customElement: { type: "string", public: true }}); #+END_SRC
      • Overwriting functions of the user: See [Overwriting functions of the User-model] #+BEGIN_SRC js const { makeModel } = require("@apparts/model"); const { createUseUser } = require("@apparts/login-server"); const { Users, User, NoUser } = createUseUser(/ , /);

        // e.g. for User class SpecialUser extends User { / overwrite stuff here / }

        module.exports = makeModel("SpecialUser", Users, SpecialUser, NoUser); #+END_SRC

** Configuration

In =db-config=

  • ~bigIntAsNumber~ must be ~true~

In =types-config=

  • ~idType~ must be ~"int"~

The configuration options for this component have to be stored accessible by the =apparts-config=-module through the name =login-config=. They are:

  • ~pwHashRounds {int}~ :: Amount of hash-rounds for password storing, passend on to =bcrypt=
  • ~tokenLength {int}~ :: Length of login-token
  • ~welcomeMail {object}~ :: The object should have keys ~title~ and ~body~. Within both, all occurrences of the string "##NAME##" will be replaced with the users name. In the body, all occurences of the string "##URL##" will be replaced with a URL for setting the password and thus confirming the email address.
  • ~resetPWMail {object}~ :: The object should have keys ~title~ and ~body~. Within both, all occurrences of the string "##NAME##" will be replaced with the users name. In the body, all occurences of the string "##URL##" will be replaced with a URL for resetting the password.
  • ~resetUrl {string}~ :: The URL, that will be send emails for setting and resetting passwords. Appended query parameters:
    • ~token {string}~ :: An auth token
    • ~?welcome {bool}~ :: Set to true, if this is not from a password reset mail, but from a welcome mail.
  • ~extraTypes {?object}~ :: An optional object that contains type definitions as expected by the preperator of [https://github.com/phuhl/apparts-types#usage]. Will be injected into the ~body~-type definitions of ~POST /v/1/user~ and can be used to validate extra parameters on user creation. These extra parameters will be passed to the ~setExtra~ function (see [Overwriting the User-model]).

*** Defining password policies

To require certain passwords, you can overwrite the =setPw= function of the user model. The new function should validate the password requirements and in case of success call =await super.setPw(password)= to set the password. In case of rejection a =PasswordNotValidError= should be thrown.

#+BEGIN_SRC js const { PasswordNotValidError } = require("@apparts/login-server");

//...

async setPw(password) { if (password.length < 10) { throw new PasswordNotValidError("Password must be 10+ characters"); } await super.setPw(password); return this; } #+END_SRC

*** Login: Exponential Backoff

To archive exponential backoff you need to create a database table =logins= and use the =createUseLogins= function to generate the model.

Then you can overwrite the =checkAuthPw= function of the user model:

#+BEGIN_SRC js const { createUseLogins } = require("@apparts/login-server"); const { useLogin } = createUseLogins();

const { checkAuthPwExponential } = require("@apparts/login-server");

//...

checkAuthPw(password) { return checkAuthPwExponential( this._dbs, useLogin, this.content.id, password, (password) => super.checkAuthPw(password) ); } #+END_SRC

*** Overwriting functions of the User-model

The user model can be overwritten to provide extra functionality. For more information on how to overwrite functions of the user model, see the documentation of [https://github.com/phuhl/apparts-model#usage]. All of these functions are only called on the OneModel of the user, thus only the ~User~ has to be extended, not the ~Users~ or ~NoUser~ classes. The functions, explicitly intended for overwriting:

  • ~getWelcomeMail() {object}~ :: Returns the content of a welcome email that is send after registration. The function returns an object of the form ~{ title: {string}, body: {string}}~. The function can access ~this.content~. It's content should contain a link with the reset token. Default implementation: #+BEGIN_SRC js getWelcomeMail() { return { title: welcomeMail.title, body: welcomeMail.body .replace( /##URL##/g, resetUrl + ?token=${encodeURIComponent( this.content.tokenforreset )}&email=${encodeURIComponent(this.content.email)}&welcome=true ), }; } #+END_SRC
  • ~getResetPWMail() {object}~ :: Returns the content of a reset password email. The function returns an object of the form ~{ title: {string}, body: {string}}~. The function can access ~this.content~. It's content should contain a link with the reset token. Default implementation: #+BEGIN_SRC js getResetPWMail() { return { title: resetMail.title, body: resetMail.body.replace( /##URL##/g, resetUrl + ?token=${encodeURIComponent( this.content.tokenforreset )}&email=${encodeURIComponent(this.content.email)} ), }; } #+END_SRC
  • ~async setExtra(extraParams) {void}~ :: This function is called on user creation. It receives as parameter all the body parameters (except for ~email~) that where present on the call of ~POST /v/1/user~. It can set the values into ~this.content~. The content will be saved afterwards automatically. To validate the types of the values, you also can configure ~extraTypes~ (see [Configuration]).
  • ~async getExtraAPITokenContent() {?object}~ :: This function can be used to inject extra information into the APIToken. Useful for providing a JWT that contains all necessary information for the API and thus reducing the amount of database calls.
  • ~async deleteMe() {void}~ :: This function can be overwritten to perform the necessary actions on deletion. Call the super function when overwriting!

** Provided REST-API

*** Create a user: POST =/v/1/user/=

  • Body Parameters
    • ~email {email}~ :: Email
  • Returns
    • 200, ~"ok"~
    • 413, ~"User exists"~

After successfully calling this API, an email will be send to ~email~, containing a link for verifying the email. This link contains a token that can be used for the reset password API and thus can be used to set the password.

*** Get user info: GET =/v/1/user=

Returns the user info. All values that are set to public (see [https://github.com/phuhl/apparts-model#usage]) in the ~extraTypes~ (see [Configuration]) are also returned.

  • Headers
    • =Authorization= with =Basic base64(username:token)=
  • Returns
    • 200, ~{ id: {id}, email: {string}, ...public extra }~
    • 400, ~"Authorization wrong"~
    • 401, ~"Unauthorized"~
    • 401, ~"User not found""~

*** Login: GET =/v/1/user/login=

  • Headers
    • =Authorization= with =Basic base64(username:password)=
  • Returns
    • 200, : { : type: "object", : values: { : id: { type: "id" }, : loginToken: { type: "base64" }, : apiToken: { type: "string" }, : }, : }
    • 400, ~"Authorization wrong"~
    • 401, ~"Unauthorized"~
    • 401, ~"User not found""~

*** Refresh API Token: GET =/v/1/user/apiToken=

  • Headers
    • =Authorization= with =Bearer loginToken=
  • Returns
    • 200, : { : type: "string" : }
    • 400, ~"Authorization wrong"~
    • 401, ~"Unauthorized"~
    • 401, ~"User not found""~

*** Update user: PUT =/v/1/user=

Update the user. All extra info must be updated over custom written APIs. Checking the password for a special password policy must be done by overwriting the ~async setPw(password)~ function. An example for checking for a minimum password length: #+BEGIN_SRC js async setPw(password) { if (password.length <= 8) { throw new HttpError(400, "Password too short"); }

return await super.setPw(password);

} #+END_SRC

TODO: update email with verification email.

  • Body Parameters
    • ~password {password}~ :: Optional, the new password
  • Headers
    • =Authorization= with =Basic base64(username:token)=.
      • Token can either be the ~loginToken~ or a ~tokenforreset~
  • Returns
    • 200, : { : type: "object", : values: { : id: { type: "id" }, : loginToken: { type: "base64" }, : apiToken: { type: "string" }, : }, : }
    • 400, ~"Authorization wrong"~
    • 400, ~"Nothing to update"~
    • 400, ~"Password required"~
    • 401, ~"Unauthorized"~
    • 401, ~"User not found""~

*** Request password reset: POST =/v/1/user/:email/reset=

  • Path Parameters
    • ~email {email}~ :: Email of the user to be changed
  • Returns
    • 200, ~"ok"~
    • 404, ~"User not found"~

*** Delete a user: DELETE =/v/1/user=

This function does not delete the user. It only disables access to the login server API in any way. To the outside it should not be visible, if the user is disabled or non-existing. To delete a user, overwrite the ~async deleteMe() {void}~ (see [Overwriting the User-model]) function of the User object. The reason for this is, that the use of foreign keys in databases might be disturbed by deleting the entity from the database.

  • Headers
    • =Authorization= with =Basic base64(username:password)=
  • Returns
    • 200, ~"ok"~
    • 400, ~"Authorization wrong"~
    • 401, ~"Unauthorized"~
    • 401, ~"User not found"~
  • Flows

** Signup

#+BEGIN_SRC plantuml :file signup.png :exports results skinparam roundcorner 5 skinparam monochrome true skinparam shadowing false actor User

group Signup User -> Loginservice : POST /v/1/user activate Loginservice Loginservice -> Mailserver : Send mail with token activate Mailserver User <-- Loginservice : "ok" deactivate Loginservice User <-- Mailserver : Mail with token deactivate Mailserver

User -> Loginservice : PUT /v/1/user token activate Loginservice User <-- Loginservice : { JWT, loginToken } deactivate Loginservice end #+END_SRC

#+RESULTS: [file:signup.png] ** Login and API-flow

#+BEGIN_SRC plantuml :exports results :file login.png actor User skinparam roundcorner 5 skinparam monochrome true skinparam shadowing false

group Login User -> Loginservice : GET /v1/user/login PW activate Loginservice User <-- Loginservice : { JWT, loginToken } deactivate Loginservice end

group API request User -> API : api request JWT activate API API --> User : response deactivate API note right The API does not need to contact the Loginservice, as all required data is in the JWT end note end

group Refresh token

... JWT expire time reached ...

User -> API : api request stale JWT Activate API User <--x API : 401 deactivate API

User -> Loginservice : GET /v1/user/apiToken loginToken activate Loginservice User <-- Loginservice : JWT deactivate Loginservice

User -> API : api request with JWT activate API API --> User : response deactivate API end

#+END_SRC

#+RESULTS: [file:login.png]

** Password reset

#+BEGIN_SRC plantuml :file resetpw.png :exports results actor User skinparam roundcorner 5 skinparam monochrome true skinparam shadowing false

User -> Loginservice : GET /v1/user/login wrong PW activate Loginservice User <--x Loginservice : 401 deactivate Loginservice

User -> Loginservice : POST /v/1/user/:email/reset activate Loginservice Loginservice -> Mailserver : Send mail with token activate Mailserver User <-- Loginservice : "ok" deactivate Loginservice User <-- Mailserver : Mail with token deactivate Mailserver

User -> Loginservice : PUT /v/1/user token activate Loginservice User <-- Loginservice : { JWT, loginToken } deactivate Loginservice

#+END_SRC

#+RESULTS: [file:resetpw.png]

3.0.1

2 years ago

3.0.0

2 years ago

2.3.0

2 years ago

2.2.0

2 years ago

2.3.1

2 years ago

2.0.2

2 years ago

2.1.0

2 years ago

2.0.1

2 years ago

2.0.0

3 years ago

1.2.0

3 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.0.0

4 years ago