Authentification et Autorisation GraphQL/Parse Server

Dans un article prĂ©cĂ©dent nous avons vu comment utiliser GraphQL avec Parse Server. Maintenant nous allons voir comment restreindre l'accĂšs aux utilisateurs non authentifiĂ©s et comment limiter les rĂ©sultats des requĂȘtes aux donnĂ©es auxquelles ils ont accĂšs.

Notre exemple sera le suivant l'api permettra de créer des utilisateurs, une fois authentifiés ils pourront poster des messages, récupérer l'ensemble de leurs messages.

Commençons par ajouter nos types d'objets dans notre schema :

/********* Object Types *********/

const userType = new GraphQLObjectType({
  name: 'User',
  description: 'A simple user',
  fields: () => ({
    id: {
      type: GraphQLID,
      resolve: obj => obj.id,
    },
    username: {
      type: GraphQLString,
      resolve: obj => obj.get('username'),
    },
    sessionToken: {
      type: GraphQLString,
      resolve: obj => obj.getSessionToken(),
    },
  }),
})

const postType = new GraphQLObjectType({
  name: 'Post',
  description: 'A simple post message',
  fields: () => ({
    id: {
      type: GraphQLID,
      resolve: obj => obj.id,
    },
    message: {
      type: GraphQLString,
      resolve: obj => obj.get('message'),
    },
    author: {
      type: userType,
      resolve: obj => obj.get('author'),
    },
  }),
})

Nous avons nos deux types d'objet User et Post. PrĂ©cision pour le type User le champ sessionToken n'est accessible que lorsqu'on retourne l'utilisateur aprĂšs la crĂ©ation ou la connexion, ensuite ce champs n'est plus accessible sa valeur sera toujours null. Nous devrons le stockĂ© cotĂ© client pour pouvoir authentifier nos requĂȘtes. Le champ author du type Post est un pointer vers le type User.

Création et connexion des utilisateurs

Avant de passer Ă  l'authentification des requĂȘtes nous allons d'abord crĂ©er deux mutations qui vont nous permettre de crĂ©er un nouvel utilisateur et lui permettre de se connecter.

/********* Mutation Types *********/
const signUp = {
  type: userType,
  description: 'Create a new user',
  args: {
    username: {
      type: GraphQLString,
    },
    email: {
      type: GraphQLString,
    },
    password: {
      type: GraphQLString,
    },
  },
  resolve: (value, { username, email, password }) => {
    var user = new Parse.User()
    user.set('username', username)
    user.set('password', password)
    user.set('email', email)
    return user.signUp()
  },
}

const login = {
  type: userType,
  description: 'Connects the user',
  args: {
    username: {
      type: GraphQLString,
    },
    password: {
      type: GraphQLString,
    },
  },
  resolve: (value, { username, password }) => {
    var user = new Parse.User()
    user.set('username', username)
    user.set('password', password)
    return Parse.User.logIn(username, password)
  },
}

Authentification

Pour authentifier les utilisateurs nous allons nous servir du sessionToken retourné lors de la création ou de la connexion.

Nous allons avoir deux maniÚres différentes de récupérer ce token :

  • Par requĂȘte: Dans ce cas la nous passons le sessionToken en tant qu'argument de la requĂȘte
  • Par contexte : Dans ce cas nous ajoutons le token dans le header de la requĂȘte HHTP et ensuite on le rĂ©cupĂšre dans nos resolver par le context de graphQL.

Exemple rĂ©cupĂ©ration par requĂȘte

const getPosts = {
  type: new GraphQLList(postType),
  args: {
    sessionToken: {
      type: GraphQLString,
    },
  },
  description: 'list of posts',
  resolve: (value, { sessionToken }, context) => {
    /* validate sessionToken */
    /* make query */
  },
}

Exemple récupération par contexte

La premiÚre chose à faire dans ce cas est de récupérer la valeur du token dans le header. On effectue cela lorsqu'on définit le serveur graphQL :

//GraphQL
app.use(
  '/graphql',
  GraphQLHTTP(request => {
    return {
      graphiql: true,
      pretty: true,
      schema: schema,
      context: { sessionToken: request.headers['x-parse-session-token'] },
    }
  })
)

Puis on rĂ©cupĂšre le sessionToken dans la requĂȘte de cette maniĂšre :

const getPosts = {
  type: new GraphQLList(postType),
  args: {
    sessionToken: {
      type: GraphQLString,
    },
  },
  description: 'list of posts',
  resolve: (value, args, { sessionToken }) => {
    /* validate sessionToken */
    /* make query */
  },
}

Astuce : Si vous testez ce systĂšme avec GraphiQL vous ne pouvez pas ajouter le sessionToken dans le header de la requĂȘte. La solution est d'utiliser cette extension chrome qui vous permettra d'ajouter des paramĂštres dans le header de vos requĂȘtes.

Restriction d'accĂšs

Maintenant nous allons voir comment nous pouvons limiter une requĂȘte seulement aux utilisateurs connectĂ©s.

Pour cela nous allons ajouter une fonction nommée isAuthorized :

const isAuthorized = async token => {
  const q = new Parse.Query(Parse.Session)
    .include('user')
    .equalTo('sessionToken', token)
  const session = await q.first({ useMasterKey: true })
  if (typeof session === 'undefined') {
    throw new Error('Unauthorized')
  }
  return session
}

Pour dĂ©terminer si l'utilisateur est autorisĂ© Ă  exĂ©cuter la requĂȘte on passe Ă  la fonction le sessionToken. Ensuite on recherche (Parse.Query) une session qui a pour sessionToken le token passĂ© en paramĂštre. Etant donnĂ©e qu'une session Parse possĂšde un pointer vers l'utilisateur de la session on inclut dans la requĂȘte la rĂ©cupĂ©ration des donnĂ©es de l'utilisateur. Enfin on test si une session a Ă©tĂ© trouvĂ©e si oui on retourne la session sinon on lĂšve une erreur.

Il ne reste plus qu'Ă  ajouter cette fonction dans chaque resolver des requĂȘtes ou mutations que l'on souhaite restreindre.

Restriction des données

Pour terminer cet article nous allons rajouter une contrainte sur la récupération des données.

D'abord nous allons ajouter une mutation qui permettra d'ajouter des messages.

const createPost = {
  type: postType,
  description: 'add new post',
  args: {
    message: {
      type: GraphQLString,
    },
  },
  resolve: async (value, { message }, { sessionToken }) => {
    const session = await isAuthorized(sessionToken)
    var Post = Parse.Object.extend('Post')
    var post = new Post()
    post.set('message', message)
    post.set('author', session.get('user'))
    post.setACL(new Parse.ACL(session.get('user')))
    return post.save()
  },
}

La mutation possÚde un argument qui est la valeur du message, ensuite on restreint l'accÚs à cette mutation aux utilisateurs authentifiés avec la fonction isAuthorized. Enfin on crée le nouveau post et restreint l'accÚs en lecture et écriture à l'utilisateur qui crée le post à l'aide de ACL.

Maintenant on vient modifier la requĂȘte qui retourne les posts pour qu'elle ne retourne que les posts que l'utilisateur a crĂ©Ă©.

const getPosts = {
  type: new GraphQLList(postType),
  description: 'list of posts',
  resolve: async (value, args, { sessionToken }) => {
    const session = await isAuthorized(sessionToken)
    var Post = Parse.Object.extend('Post')
    return new Parse.Query(Post).include('author').find({ sessionToken })
  },
}

Ici premiĂšrement on restreint l'accĂšs aux utilisateurs connectĂ©s puis on effectue la requĂȘte de rĂ©cupĂ©ration des posts. Pour limiter les rĂ©sultats il nous suffit de passer le sessionToken dans la mĂ©thode find et Parse s'occupera de retourner que les posts d'ont l'utilisateur Ă  accĂšs.

Et voila nous en avons fini avec l'authentification et l'autorisation avec GraphQL et Parse Server.Vous pouvez retrouver l'ensemble du code source ici.