Add MSAL to ApolloServer

Install libraries to validate tokens by opening a console in data-access project and issuing the following commands

/azure-quickstart/data-access/
npm install jose
npm install openid-client

In /azure-quickstart/data-access/ create the directories sharedCode/auth .

In the auth directory, create a new file msal.ts and add the following code.

/azure-quickstart/data-access/shared-code/auth/msal.ts
import { Issuer, Client } from 'openid-client';
import {JWT} from 'jose';

var verifyAccessToken = async (context) : Promise<[object, boolean]>  => {
  let token = context.request.headers["authorization"];
  if (!token || !token.startsWith("Bearer ")) return [{}, false];

  token = token.slice(7, token.length).trimLeft(); // Remove 'Bearer ' characters from start of Auth header value

  const settings = {
    audience: process.env.AAD_TOKEN_APPLICATION_ID,
    openIdConfigUrl: process.env.AAD_TOKEN_OPEN_ID_CONNECT_METADATA_DOCUMENT,
    tenantId: process.env.AAD_TOKEN_TENANT_ID
  };

  const issuer = await Issuer.discover(settings.openIdConfigUrl);
  const keyStore = await issuer.keystore();

  var results = JWT.verify(
    token,
    keyStore, 
    {
      audience: settings.audience,
     // issuer: 
        //issuer must remain commented out if you're accepting tokens from :
        // Microsoft's public endpoint (which will be: 'https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0' or it can be any AAD tenant's ID)
        //if you only want to accept local AAD Accounts use: `https://login.microsoftonline.com/${settings.tenantId}/v2.0`
    }
  );
  
  return [
    {
      "authToken": results,
    },
    true
  ];
}

export default {
  VerifyAccessToken: verifyAccessToken
}

Replace the contents of the main function file with the following to properly reference MSAL and to allow for auth headers.

/azure-quickstart/data-access/graphql/index.ts
let appInsights = require("applicationinsights");
appInsights.setup().start();
appInsights.defaultClient.commonProperties = {
  environment: process.env.WEBSITE_HOSTNAME,
  functionArea: "graphql"
};
let appInsightsClient = appInsights.defaultClient;

import {
  ApolloServerPlugin,
  GraphQLRequestContext,
  GraphQLRequestListener,
} from 'apollo-server-plugin-base';

import { ApolloServer, gql }  from 'apollo-server-azure-functions';
import MsalAuth from '../shared/auth/msal';
import { HttpRequest, Context } from '@azure/functions';

// Construct a schema, using GraphQL schema language
const typeDefs = gql`
  type Query {
    hello: String
  }
`;

// Provide resolver functions for your schema fields
const resolvers = {
  Query: {
    hello: (parent,args,context) => {
      return `Hello world! ${JSON.stringify(context)}`
    },
  },
};

// referenced from https://jeffmagnusson.com/post/graphql-apollo-server-plugins-in-typescript
const appInsightsPlugin = <ApolloServerPlugin & GraphQLRequestListener>{

  // Fires whenever a GraphQL request is received from a client.
  requestDidStart(requestContext:GraphQLRequestContext): GraphQLRequestListener | void{
    appInsightsClient.trackMetric({name: "apollo-query", value: 1});
    return this;
  },
  // Fires for graph exceptions
  didEncounterErrors: function(requestContext:GraphQLRequestContext) {
    appInsightsClient.trackMetric({name: "apollo-error", value: 1});
    appInsightsClient.trackException({exception: new Error("Apollo Error")});
    appInsightsClient.trackException({exception: {category:"Apollo Error", details: requestContext.errors}});
  }
  
}

const getPlaygroundSetting = () => {
  if(process.env.APOLLO_PLAYGROUND_VISIBLE === "true" ){
    if(process.env.APOLLO_PLAYGROUND_ENDPOINT){
      return {endpoint :process.env.APOLLO_PLAYGROUND_ENDPOINT}
    }
    return true
  } else {
    return false
  }
}

const server = new ApolloServer(
  { 
    typeDefs, 
    resolvers, 
    playground: {endpoint:process.env.APOLLO_PLAYGROUND_ENDPOINT},
    plugins: [
      appInsightsPlugin
    ],
    context: async (request) => {
      var [ user, validated ] = await MsalAuth.VerifyAccessToken(request);
      return {user, validated};
    },
  },
);

const graphqlHandler = server.createHandler({
  cors: {
    origin: '*',
    credentials: true,
  },
});

export default (context: Context, req: HttpRequest) => {
  // https://github.com/Azure/azure-functions-host/issues/6013
  req.headers['x-ms-privatelink-id'] = '';
  // apollo-server only reads this specific string
  req.headers['Access-Control-Request-Headers'] = req.headers['Access-Control-Request-Headers'] || req.headers['access-control-request-headers'];
  return graphqlHandler(context, req);
}

Update the local settings to supply the function with the appropriate values:

/azure-quickstart/data-access/local.settings.json
{
  ...
  "Values": [
    {
    ...
    "AAD_TOKEN_OPEN_ID_CONNECT_METADATA_DOCUMENT" : "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
    "AAD_TOKEN_APPLICATION_ID" : "<< AAD API APPPLICATION IDENTIFIER>>",
    "AAD_TOKEN_TENANT_ID" : "<< AAD API TENANT ID>>",
    
    "APOLLO_PLAYGROUND_URI" : "http://localhost:7071/api/graphql"
    ...
    }
  ]
  ...
}

Azure Settings

Open the data-access function App

  • In the function app navigate to Configuration

    • Choose + New Application Setting for each of the following

      • Name: AAD_TOKEN_OPEN_ID_CONNECT_METADATA_DOCUMENT

        • Value: <<SAME VALUE AS USED IN LOCAL SETTINGS>>

      • Name: AAD_TOKEN_APPLICATION_ID

        • Value: <<SAME VALUE AS USED IN LOCAL SETTINGS>>

      • Name: AAD_TOKEN_TENANT_ID

        • Value: <<SAME VALUE AS USED IN LOCAL SETTINGS>>

      • Name: APOLLO_PLAYGROUND_URI

        • Value: <<AZURE FRONT DOOR URL>>/api/graphql (e.g.: https://sharethrift<<random number>>.azurefd.net/api/graphql)

  • Choose Save

  • Choose Continue (wait until completion)

Repeat the same steps for the data-access-west function app.

Important Warnings

Last updated

Was this helpful?