• Docs
  • Pricing
  • Support
  • Blog
  • Login

›External Authentication

Intro

  • What's OneGraph?
  • How does it work?
  • Creating your first app
  • Making your first query
  • OneGraphiQL
  • Authentication & Security Overview
  • Custom Google Auth

On the frontend

  • Using with Apollo
  • Log in to services
  • Example with create-react-app

External Authentication

  • What are JWTs?
  • AuthGuardian
  • Securing your Apollo server
  • Securing your Hasura API
  • Securing your Netlify site
  • Securing your express.js app
  • Advanced JWT customization with webhooks

Subscriptions

  • Subscriptions
  • Webhook-based subscriptions
  • Websocket-based subscriptions
  • Salesforce subscriptions
  • GitHub subscriptions
  • Gmail Subscriptions

Advanced

  • Persisted Queries
  • Mailchimp Signup with Persisted Queries

Customizing JWTs with webhooks

When we need extreme customization of JWTs for our users, we can use a preflight-query and a webhook combination for absolute control.

Two-step process to customize our JWTs: Data & Logic

For any app we write, authentication and authorization will be based off of two factors:

  1. Data: Information about this user, their organizations, etc.
  2. Logic: Deciding based off of #1 what - if any - permissions should be granted to this user.

The preflight query collects all the relevant data about our user, and webhook processing allows we customize the data contained in the JWT.

Step 1. Authenticating with data via OneGraph's data access

Much of a user's identity is determined by data related to them. With OneGraph we can retrieve all the relevant data every time a user successfully logs into an external service (e.g. GitHub, Spotify, etc.) by setting a preflight query.

The results of the query will be stored directly in the JWT for our application to rely on.

For example, to identify our users by their GitHub id, and also store their GitHub email (to display in the app navigation), simply set this preflight query:

query FindMe {
  me {
    github {
      email
      # Use this instead of `id`
      # for a permanent GitHub id.
      databaseId
    }
  }
}

Now every time a user logs in, OneGraph will generate and sign a JWT with the result of that query under the "https://onegraph.com/jwt/preflight-query" (one of our JWT claims), e.g.:

// This is the fullly decoded JWT payload
{
  "iss": "OneGraph",
  "aud": "https://serve.onegraph.com/dashboard/app/d1995c39-be74-4c23-868a-a263d9a54ac1",
  "iat": 1561589151,
  "exp": 1562798750,
  "https://onegraph.com/jwt/claims": {
    "access_token": "HX2sXDQZ5anYSa-zapGur3H7R3aB5n9lQaMVuvHZn2Y"
  },
  "https://onegraph.com/jwt/preflight-query": {
    "data": {
      "me": {
        "github": {
          "email": "docs@onegraph.com",
          // We can use this id in our app to authenticate this user!
          "databaseId": "35996",
        }
      }
    }
  }
}

As long as the JWT is signed correctly we can trust that whoever has this token definitely has a GitHub userId of 35996.

(Optional) Step 2: Authorizing via data and webhooks

It's common to also include the roles a user has so our app can apply the appropriate permissions and access rules.

As a simple example, assume we have two roles:

  • admin
  • user

We can configure OneGraph to first collect the result of the preflight query above, and then send it to a webhook we control for final processing. This is where we will determine the user's role.

In our app, a user is an admin if they are a member of the OneGraph organization on GitHub, otherwise they're a normal user.

We'll first expand our preflight query to include the GitHub organization information like id and name:

query FindMe {
  me {
    github {
      email
      databaseId
      organizations(first: 100) {
        nodes {
          databaseId
          name
        }
      }
    }
  }
}

The result of that query for our hypothetical user will be:

// Example query result (along with the full JWT data) that will be sent to our webhook
{
  "iss": "OneGraph",
  "aud": "https://serve.onegraph.com/dashboard/app/d1995c39-be74-4c23-868a-a263d9a54ac1",
  "iat": 1561589151,
  "exp": 1562798750,
  "https://onegraph.com/jwt/claims": {
    "access_token": "HX2sXDQZ5anYSa-zapGur3H7R3aB5n9lQaMVuvHZn2Y"
  },
  // The preflight-query result will be stored here
  "https://onegraph.com/jwt/preflight-query": {
    "data": {
      "me": {
        "github": {
          "email": "docs@onegraph.com",
          "id": 35996,
          "organizations": {
            "nodes": [
              {
                "databaseId": 3372922,
                "name": "HappyCodingCo"
              },
              {
                "databaseId": 29494709,
                "name": "OneGraph"
              }
            ]
          }
        }
      }
    }
  }
}

OneGraph will take that result and will POST it to our webhook url, which in our case is an express route handler:

app.post('/processOneGraphJwt', function (request, response) {
  // Access the preflight-query results
  const queryKey = 'https://onegraph.com/jwt/preflight-query';
  const data = request.body[queryKey] && request.body[queryKey].data;

  // Remove the query results from the JWT, we don't need them anymore
  delete request.body[queryKey];

  // Retrieve the GitHub data, being careful about nullable fields
  const me = data.me;
  const gitHub = !!me && me.github && me.github.databaseId && me.github;

  const isAdmin =
    gitHub && gitHub.organizations.nodes.find((org) => org.name === 'OneGraph');

  // Generate data specific to our app based on this
  const ourAppData = {
    allowedRoles: ['user', isAdmin ? 'admin' : null].filter(Boolean),
    defaultRole: isAdmin ? 'admin' : 'user',
    userId: gitHub && gitHub.databaseId,
  };

  // Combine the basic JWT data with our app-specific customizations
  const finalToken = {
    ...request.body,
    ourAppData,
  };

  // Return to OneGraph the final token to be signed and used by the user in our app
  response.send(finalToken);
});

If this webhook returns a successful JSON response, OneGraph will take the exact output of this webhook, sign it, and use it as the final JWT.

The final JWT would look like:

{
  "iss": "OneGraph",
  "aud": "https://serve.onegraph.com/dashboard/app/d1995c39-be74-4c23-868a-a263d9a54ac1",
  "iat": 1561591677,
  "exp": 1562801276,
  "https://onegraph.com/jwt/claims": {
    "access_token": "uIl4bpOPOvPVHqDwUWGkh1QZY_h0EyN5GsCJIv5gQEE"
  },
  "ourAppData": {
    "allowedRoles": [
      "user",
      "admin"
    ],
    "defaultRole": "admin",
    "userId": 35996
  }
}

Notice how our webhook can change any values passed to it, including the aud, exp, etc. This gives us full control over the final JWT delivered to our users.

Migrating from AuthGuardian to custom Webhooks

It's common to use AuthGuardian initially, but our app may grow until the logic and data needed means we need more control.

To smooth the transition, AuthGuardian can generate a preflight-query and the necessary JavaScript to 100% reproduce the rules you originally specified.

  1. Click on Export, and copy the GraphQL Query into
  2. Copy the preflight query into your Advanced JWT settings
  3. Copy the generated JavaScript into your Webhook can set the http address under Advanced JWT settings

Here's an example of AuthGuardian's export:

Given the rules:

Example AuthGuardian export

The preflight-query is:

query FindMe {
  gitHub {
    gitHubOrganization_onegraph: organization(login: "OneGraph") {
      viewerIsAMember
    }
    gitHubRepo_onegraph_graphiql_explorer: repository(
      name: "graphiql-explorer"
      owner: "OneGraph"
    ) {
      viewerHasStarred
    }
  }
}

And the Webhook code:

// Runtime helper functions
/* npm install --save lodash */
var _ = require('lodash');

const emailDomain = (email) => {
  if (!email) {
    return false;
  }

  const pieces = email.split('@');
  const domain = pieces[pieces.length - 1];
  // Make sure it's a reasonably well-formed email
  // then return the domain
  return email.match(/.+@.+/) && domain;
};

// JWT-processing logic
const processQueryResult = (initialJwtInput) => {
  /* These are the minimum keys for the JWT to continue working with OneGraph.
     To stop this user from using OneGraph (and upstream APIs), omit this object. */
  const finalJwt = _.pick(initialJwtInput, [
    'ass',
    'aud',
    'iat',
    'exp',
    'https://onegraph.com/jwt/claims',
  ]);

  // The result of your preflight-query on OneGraph will be under this key/claim
  const data = _.get(
    initialJwtInput,
    ['https://onegraph.com/jwt/preflight-query', 'data'],
    {},
  );

  // All the data dependencies for the rules and JWT
  const gitHubHasStarredRepository_onegraph_graphiql_explorer = _.get(data, [
    'gitHub',
    'gitHubRepo_onegraph_graphiql_explorer',
    'viewerHasStarred',
  ]);
  const gitHubIsAMemberOfOrganization_onegraph = _.get(
    data,
    ['gitHub', 'gitHubOrganization_onegraph', 'viewerIsAMember'],
    false,
  );

  /* Conditionally apply these rules */
  if (
    gitHubIsAMemberOfOrganization_onegraph &&
    !!gitHubHasStarredRepository_onegraph_graphiql_explorer
  ) {
    _.update(
      finalJwt,
      ['https://hasura.io/jwt/claims', 'x-hasura-allowed-roles'],
      (list) => (list || []).concat(['admin']),
    );
  }

  // All of our rules have been applied, we have a full JWT!
  return finalJwt;
};
← Securing your express.js appSubscriptions →
Links
OneGraph Overview Example projectsOneGraphiQL Explorer
Support
Live chat on Spectrum> TwitterBlog
More
Terms of ServicePrivacy Policy
Copyright © 2021 OneGraph