distributeaid / toolbox

DistributeAid Toolbox
http://distributeaid.org
GNU Affero General Public License v3.0
3 stars 1 forks source link

Authentication and authorisation #18

Closed pedro-gutierrez closed 3 years ago

pedro-gutierrez commented 4 years ago

Is your feature request related to a problem? Please describe.

We currently don't have an authentication and authorisation system in the toolbox. We are building a GraphQL where there is currently no access control, but in the future, once the api is ready, it will be used by different people having different roles in a shared workflow and we need to implement some security rules.

Describe the solution you'd like

We would need to discuss how we want to implement this. I think it is not impossible to support different identity providers such as Auth0 or even build our own authentication mechanism. The advantage of having our own mechanism is that we have less moving parts, it is easier to test and manage as a whole, although the security is obviously not comparable to what Auth0 or Cognito are able to offer.

1. Some ideas for a permissions model

Column DataType Comments
id int primary key
name String full name

We could imagine a given user could have more than one kind of credentials. Also, if we decide to delegate authentication to Auth0, then this table would not be needed. But we would still need a table of known users in the toolbox, that's why I am suggesting that we keep credentials separate.

Column DataType Comments
id int primary key
user_id int foreign-key
kind enum email-password, phone
data jsonb depending on the kind
Column DataType Comments
id int primary key
name string

Example of roles:

  1. da_admin: DistributeAid admins
  2. group_admin: Aid Group admins
  3. volunteer: Aid Group volunteer

We can mimic the way Auth0 models permissions:

Column DataType Comments
id int primary key
role_id int foreign key
permission string {action}:{resource}:{opts}

Examples:

  1. create:group: this gives the permission to a user to create an aid group.
  2. update:group:author: this would give the permission to a user to only update a group that he/she has previously created
Column DataType Comments
id int primary key
group_id int foreign key
user_id int foreign key
role_id int foreign key

Notes:

The bottom line is that we could have a set of permissions per user and per group. The total list of permissions is the union of all permissions of all the roles a given user has in a given group.

2. How do we create new users

We could have a new simple GraphQL mutation:

type UserInput {
  email: String!,
  password: String!
}

mutation {
  createUser(userInput: UserInput!) {
    id,
    email
  }
}

Notes:

curl --request POST \
  --url 'https://YOUR_DOMAIN/api/v2/users' \
  --header 'authorization: Bearer ABCD' \
  --header 'content-type: application/json' \
  --data '{"email": "jane.doe@example.com", "user_metadata": {"user_id": ... , "hobby": "surfing"}, "app_metadata": {"plan": "full"}}'

3. How a volunteer could register into the platform

We could have a new simple GraphQL mutation

mutation {
  registerWithEmail(email: "user@domain.com") {
    success
  }
}

where success is a boolean value.

4. How a user can ask to receive a password reset link

We could have a new simple GraphQL mutation

mutation {
  sendResetLink(email: "user@domain.com") {
    success
  }
}

This would send a reset link with a token via email. Note: we need to integrate with an email provider first.

5. How a user can reset their password

We could have a new simple GraphQL mutation

type PasswordResetInput {
  email: String!
  password: String!
  token: String!
}

mutation {
  resetPassword(passwordResetInput: PasswordResetInput!) {
    success
  }
}

Notes:

curl --request PATCH \
  --url 'https://YOUR_DOMAIN/api/v2/users/USER_ID' \
  --header 'content-type: application/json' \
  --data '{"password": "NEW_PASSWORD","connection": "Username-Password-Authentication"}'

6. How we authenticate users

We can have a simple mutation that creates a new token:

mutation {
  login(userInput: userInput!) {
    token
  }
}

If we decide to issue opaque tokens, then we will need to store them in a tokens table, and link them to a valid user:

Column DataType Comments
id int primary key
token string unique
user_id foreign key
created date time for expirations

7. How do we authenticate user requests

Tokens obtained via login, or externally via third party providers (auth0) need to be part of the request (authorisation header).

We could support multiple types of tokens:

8. How do we authorise requests with a JWT

Note: we can encode user membership to groups, via their roles, or we can encode all the permissions. However as a user belongs to an increasing number of groups, this might not scale very well and result in very long claims.

9. How do we authorise requests with an opaque token

11. How a user joins a group

We could implement a simple mutation:

type JoinRequest {
   user: User!,
   group: Group!,
   status: [pending, accepted, rejected, expired],
   createdAt,
   updatedAt
}

mutation {
  joinGroup(group: id) { 
    id,
    group {
       id
    },
    user {
       id
    },
    status
  }  
}

12. How a group admin accepts a join request

We could implement a mutation such as:

mutation {
  updateJoinGroupRequest(id: "...", status: "accepted") { 
    id,
    group {
       id
    },
    user {
       id
    },
    status
  }  
}

By accepting a request, a new role is created for that user for that group. The presence of that role defines the membership of that user in that role.

13. How do we scope range queries

We will need to protect every query that returns a range of resources, eg. get all groups, get all shipments, and return only those resources the user has actually access to. This needs further investigation.