10

I am trying to set up Auth0's universal login with my Graphcool, Apollo and React app. My site is a SPA statically hosted with Netlify.

I am using Graphcool's official Auth0 template: https://github.com/graphcool/templates/tree/master/auth/auth0

However, I cannot find much detail in the official documents so I am following this tutorial: https://medium.com/@quiaro/authentication-inside-a-graphcool-service-using-auth0-5056806d02f0

I have an auth component which is called by the login component:

import React from 'react';
import auth0 from 'auth0-js';

export default class Auth {
    auth0 = new auth0.WebAuth({
        domain: 'MY-SITE.auth0.com',
        clientID: 'MY-CLIENT-ID',
        redirectUri: 'http://localhost:3000/callback',
        audience: 'https://MY-SITE.auth0.com/userinfo',
        responseType: 'id_token',
        scope: 'openid email',
    });

    login() {
        this.auth0.authorize();
    }
}

import Auth from './Auth';
class Login extends React.Component {
    render() {
        const auth = new Auth();
        auth.login();
        return (
            // Login message
        );
    }
}

As expected this takes me to Auth0's page where I can login, after which I am redirected back to my site with parameters added to the url:

http://localhost:3000/callback#id_token=LONG-ID-TOKEN&state=STRING

If I paste the LONG-ID-TOKEN into https://jwt.io/ the information appears to be correct and it says the signature is valid:

Header:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "STRING"
}

Payload:

{
  "email": "test@gmail.com",
  "email_verified": false,
  "iss": "MY-SITE.auth0.com",
  "sub": "auth0|STRING",
  "aud": "MY-CLIENT-ID",
  "iat": INTEGER,
  "exp": INTEGER,
  "at_hash": "STRING",
  "nonce": "STRING"
}

In the playground in Graphcool's console I test the token:

mutation {
  authenticateUser(accessToken:"LONG-ID-TOKEN") {
    id
    token
  }
}

But the result is an error. I get the same error if I enter a random string as the token value.

{
  "data": null,
  "errors": [
    {
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "functionError": "An unexpected error occured",
      "path": [
        "authenticateUser"
      ],
      "code": 5001,
      "message": "function execution error: An unexpected error occured",
      "requestId": "us-west-2:simple:STRING"
    }
  ]
}

From digging around I noticed that the official templates are different to those used in the tutorial:

Tutorial:

type AuthenticatedUser {
  id: String!
  token: String!
}

extend type Mutation {
  authenticateUser(idToken: String!): AuthenticatedUser!
}

Official:

type AuthenticateUserPayload {
  id: String!
  token: String!
}

extend type Mutation {
  authenticateUser(accessToken: String!): AuthenticateUserPayload!
}

Tutorial:

const jwt = require('jsonwebtoken');
const jwkRsa = require('jwks-rsa');
const fromEvent = require('graphcool-lib').fromEvent;

const verifyToken = token =>
  new Promise((resolve, reject) => {
    // Decode the JWT Token
    const decoded = jwt.decode(token, { complete: true });
    if (!decoded || !decoded.header || !decoded.header.kid) {
      reject('Unable to retrieve key identifier from token');
    }
    if (decoded.header.alg !== 'RS256') {
      reject(
        `Wrong signature algorithm, expected RS256, got ${decoded.header.alg}`
      );
    }
    const jkwsClient = jwkRsa({
      cache: true,
      jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
    });

    // Retrieve the JKWS's signing key using the decode token's key identifier (kid)
    jkwsClient.getSigningKey(decoded.header.kid, (err, key) => {
      if (err) return reject(err);

      const signingKey = key.publicKey || key.rsaPublicKey;

      // Validate the token against the JKWS's signing key
      jwt.verify(
        token,
        signingKey,
        {
          algorithms: ['RS256'],
          ignoreExpiration: false,
          issuer: `https://${process.env.AUTH0_DOMAIN}/`,
          audience: `${process.env.AUTH0_CLIENT_ID}`,
        },
        (err, decoded) => {
          if (err) return reject(err);
          resolve(decoded);
        }
      );
    });
  });

//Retrieves the Graphcool user record using the Auth0 user id
const getGraphcoolUser = (auth0UserId, api) =>
  api
    .request(
      `
        query getUser($auth0UserId: String!){
          User(auth0UserId: $auth0UserId){
            id
          }
        }
      `,
      { auth0UserId }
    )
    .then(queryResult => queryResult.User);

//Creates a new User record.
const createGraphCoolUser = (auth0UserId, email, api) =>
  api
    .request(
      `
        mutation createUser($auth0UserId: String!, $email: String) {
          createUser(
            auth0UserId: $auth0UserId
            email: $email
          ){
            id
          }
        }
      `,
      { auth0UserId, email }
    )
    .then(queryResult => queryResult.createUser);

export default async event => {
  if (!process.env.AUTH0_DOMAIN) {
    return { error: 'Missing AUTH0_DOMAIN environment variable' };
  }
  if (!process.env.AUTH0_CLIENT_ID) {
    return { error: 'Missing AUTH0_CLIENT_ID environment variable' };
  }

  try {
    const { idToken } = event.data;
    const graphcool = fromEvent(event);
    const api = graphcool.api('simple/v1');

    const decodedToken = await verifyToken(idToken);
    let graphCoolUser = await getGraphcoolUser(decodedToken.sub, api);

    //If the user doesn't exist, a new record is created.
    if (graphCoolUser === null) {
      graphCoolUser = await createGraphCoolUser(
        decodedToken.sub,
        decodedToken.email,
        api
      );
    }

    // custom exp does not work yet, see https://github.com/graphcool/graphcool-lib/issues/19
    const token = await graphcool.generateNodeToken(
      graphCoolUser.id,
      'User',
      decodedToken.exp
    );

    return { data: { id: graphCoolUser.id, token } };
  } catch (err) {
    return { error: err };
  }
};

Official:

const isomorphicFetch = require('isomorphic-fetch')
const jwt = require('jsonwebtoken')
const jwkRsa = require('jwks-rsa')
const fromEvent = require('graphcool-lib').fromEvent

//Validates the request JWT token
const verifyToken = token =>
  new Promise(resolve => {
    //Decode the JWT Token
    const decoded = jwt.decode(token, { complete: true })
    if (!decoded || !decoded.header || !decoded.header.kid) {
      throw new Error('Unable to retrieve key identifier from token')
    }
    if (decoded.header.alg !== 'RS256') {
      throw new Error(
        `Wrong signature algorithm, expected RS256, got ${decoded.header.alg}`
      )
    }
    const jkwsClient = jwkRsa({
      cache: true,
      jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
    })
    //Retrieve the JKWS's signing key using the decode token's key identifier (kid)
    jkwsClient.getSigningKey(decoded.header.kid, (err, key) => {
      if (err) throw new Error(err)
      const signingKey = key.publicKey || key.rsaPublicKey
      //If the JWT Token was valid, verify its validity against the JKWS's signing key
      jwt.verify(
        token,
        signingKey,
        {
          algorithms: ['RS256'],
          audience: process.env.AUTH0_API_IDENTIFIER,
          ignoreExpiration: false,
          issuer: `https://${process.env.AUTH0_DOMAIN}/`
        },
        (err, decoded) => {
          if (err) throw new Error(err)
          return resolve(decoded)
        }
      )
    })
  })

//Retrieves the Graphcool user record using the Auth0 user id
const getGraphcoolUser = (auth0UserId, api) =>
  api
    .request(
      `
        query getUser($auth0UserId: String!){
          User(auth0UserId: $auth0UserId){
            id
          }
        }
      `,
      { auth0UserId }
    )
    .then(queryResult => queryResult.User)

//Creates a new User record.
const createGraphCoolUser = (auth0UserId, email, api) =>
  api
    .request(
      `
        mutation createUser($auth0UserId: String!, $email: String) {
          createUser(
            auth0UserId: $auth0UserId
            email: $email
          ){
            id
          }
        }
      `,
      { auth0UserId, email }
    )
    .then(queryResult => queryResult.createUser)

const fetchAuth0Email = accessToken =>
  fetch(
    `https://${process.env.AUTH0_DOMAIN}/userinfo?access_token=${accessToken}`
  )
    .then(response => response.json())
    .then(json => json.email)

export default async event => {
  try {
    if (!process.env.AUTH0_DOMAIN || !process.env.AUTH0_API_IDENTIFIER) {
      throw new Error(
        'Missing AUTH0_DOMAIN or AUTH0_API_IDENTIFIER environment variable'
      )
    }
    const { accessToken } = event.data

    const decodedToken = await verifyToken(accessToken)
    const graphcool = fromEvent(event)
    const api = graphcool.api('simple/v1')

    let graphCoolUser = await getGraphcoolUser(decodedToken.sub, api)
    //If the user doesn't exist, a new record is created.
    if (graphCoolUser === null) {
      // fetch email if scope includes it
      let email = null
      if (decodedToken.scope.includes('email')) {
        email = await fetchAuth0Email(accessToken)
      }
      graphCoolUser = await createGraphCoolUser(decodedToken.sub, email, api)
    }

    // custom exp does not work yet, see https://github.com/graphcool/graphcool-lib/issues/19
    const token = await graphcool.generateNodeToken(
      graphCoolUser.id,
      'User',
      decodedToken.exp
    )

    return { data: { id: graphCoolUser.id, token } }
  } catch (err) {
    console.log(err)
    return { error: 'An unexpected error occured' }
  }
}

I would rather user the official templates. Is the approach taken by the tutorial still going to work?

Thanks!

mstill3
  • 132
  • 1
  • 2
  • 13
Evanss
  • 23,390
  • 94
  • 282
  • 505

1 Answers1

0

Approach shown in the tutorial (using id_token to access api) is insecure and not recommended anymore. Please refer to this article to understand why id_token should not be used to access API https://auth0.com/blog/why-should-use-accesstokens-to-secure-an-api/

kachhalimbu
  • 2,190
  • 17
  • 14