FAQ | This is a LIVE service | Changelog

Skip to content
Snippets Groups Projects

API Gateway example

  • Clone with SSH
  • Clone with HTTPS
  • Embed
  • Share
    The snippet can be accessed without any authentication.
    Authored by Dr Rich Wareham

    An example of using the API Gateway to fetch a user's Lookup profile.

    Edited
    lookup.ts 8.43 KiB
    #!/usr/bin/env -S deno run --allow-net --allow-read --allow-run
    // ^- This is some Unix-magic to allow running this script as a stand alone
    // program. On Windows and other non-Unix OSes, run this script as:
    //
    //    deno run --allow-net --allow-read --allow-run lookup.ts
    //
    // Deno is a tool that lets you run JavaScript locally. Installation
    // instructions for Linux, macOS and Windows are at
    // https://deno.land/manual@v1.15.3/getting_started/installation.
    //
    // This script demonstrates calling the Lookup API published at the API Gateway
    // and authenticating as a normal user rather than as an Application. In order
    // to demonstrate this, the script will print out the authenticated user"s
    // profile on Lookup including any private fields.
    //
    // You must have already generated a client id (or "key") for an App on
    // https://developer.api.apps.cam.ac.uk/ and enabled the "Lookup" API for your
    // App. The "callback URL" for the App must be
    // "http://localhost:4000/oauth2/callback". See the documentation linked to on
    // https://developer.api.apps.cam.ac.uk/ for more information on this.
    
    //// DEPENDENCIES ////
    
    // "open" allows opening URLs in the user"s default browser.
    import { open } from "https://deno.land/x/open/index.ts"
    
    // "Server" is used to implement a small HTTP server which listens for the
    // authentication response from the API Gateway.
    import { Server } from "https://deno.land/std/http/server.ts"
    
    // Import an implementation of the authentication protocol used by the API
    // Gateway from the Microsoft Authentication Library (MSAL).
    import { PublicClientApplication, ProtocolMode } from "https://deno.land/x/azure_msal_deno@v1.1.0/mod.ts"
    
    // The API Gateway makes use of Proof Key for Code Exchange (PKCE) to increase
    // security. Using PKCE is best practice for all clients.
    //
    // See: https://datatracker.ietf.org/doc/html/rfc7636)
    import { create as createPKCEVerifierPair } from "https://deno.land/x/pkce_deno/mod.ts"
    
    // The API Gateway will pass us back a random "state" token we give it. We use
    // this to verify that the request we got back from the gateway matches the
    // authorisation we triggered.
    import { cryptoRandomString } from "https://deno.land/x/crypto_random_string@1.0.0/mod.ts"
    
    //// CONSTANTS ////
    
    // The client id registered for the App on the API Gateway developer portal.
    // This is not a secret.
    const clientId = "s7w86kczmWEUncn7oerwAt23BC4zaE9u"
    
    // Create a new client application using the Microsoft Sign-in library.
    console.log("Creating the client object to perform a sign in.")
    const client = new PublicClientApplication({
      auth: {
        authorityMetadata: JSON.stringify({
          // These endpoints can be found at https://api.apps.cam.ac.uk/oauth2/v1/.well-known/openid-configuration
          // or at https://developer.api.apps.cam.ac.uk/docs/oauth2/1/overview.
          authorization_endpoint: "https://api.apps.cam.ac.uk/oauth2/v1/auth",
          token_endpoint: "https://api.apps.cam.ac.uk/oauth2/v1/token",
        }),
        clientId,
        knownAuthorities: ["api.apps.cam.ac.uk"],
        protocolMode: ProtocolMode.OIDC,
      }
    })
    
    //// LOGIC ////
    
    // Some common parameters we need to pass to the API Gateway as part of the
    // authentication dance.
    //
    // Passing "https://api.apps.cam.ac.uk/lookup" as the scope means we"ll ask the
    // user for permission to use Lookup on their behalf.
    //
    // The App registered at https://developer.api.apps.cam.ac.uk/ _must_ have the
    // callback URL set to "http://localhost:4000/oauth2/callback".
    const authParams = {
      scopes: ["https://api.apps.cam.ac.uk/lookup"],
      redirectUri: "http://localhost:4000/oauth2/callback",
    }
    
    // Create some random tokens required for PKCE and security best practice.
    const state = cryptoRandomString({ length: 64, type: "alphanumeric" })
    const nonce = cryptoRandomString({ length: 64, type: "alphanumeric" })
    const { codeVerifier, codeChallenge } = createPKCEVerifierPair()
    
    // A handler function to handle requests received back from the API Gateway.
    const handleRequests = async (request: Request) => {
      // Pull out the path (the part before "?") from the request URL.
      const url = new URL(request.url)
      const path = url.pathname
    
      if(path === "/oauth2/callback") {
        // Handle the response. (See below.)
        handleCodeResponse(url)
    
        // Thank the user.
        return new Response(
          "Thanks, you can now close the browser tab and look for the result on the console."
        )
      } else {
        // Otherwise, return "not found"
        return new Response("Not found", { status: 404 })
      }
    }
    
    // Make a new web server to listen to requests from the Gateway.
    const server = new Server({
      addr: "0.0.0.0:4000",
      handler: handleRequests
    })
    
    // This is a "Promise" which we can "await". The "await" will continue once the
    // server stops.
    const serverPromise = server.listenAndServe()
    
    // We start the authentication by getting the user to visit a URL generated by
    // the MSAL client.
    const authUrl = await client.getAuthCodeUrl({
      ...authParams,
      codeChallenge,
      codeChallengeMethod: "S256",
      state,
      nonce,
    })
    
    // Tell the user to visit the URL.
    console.log("Visit the following URL in your browser. I'll also try to open it")
    console.log("for you in your default browser.\n")
    console.log(`     ${authUrl}\n`)
    
    // Also try opening the URL in the user"s preferred browser.
    await open(authUrl, {
      wait: false, url: Deno.build.os === "windows"
    })
    
    // A handler function called when we"ve got a response back from the API
    // gateway with a code.
    const handleCodeResponse = async (url: URL) => {
      // We should have been redirected back to a URL of the form
      //
      //    http://localhost:4000/oauth2/callback?code={...}&state={...}
      //
      // Pull the code and state out of the URL.
      const code = url.searchParams.get("code")
      const receivedState = url.searchParams.get("state")
    
      // We should"ve got a code.
      if(!code) {
        throw new Error("No code returned from API Gateway.")
      }
    
      // The state should match the one we sent with the original authentication
      // request.
      if(receivedState !== state) {
        throw new Error("Incorrect state received from API Gateway.")
      }
    
      // Get an access token in exchange for the code.
      console.log("Exchanging the auth code from the Gateway for a token...")
      const authenticationResult = await client.acquireTokenByCode({
        ...authParams,
        code,
        codeVerifier,
      })
    
      // Check we got a response.
      if(!authenticationResult) {
        throw new Error("No response from API Gateway.")
      }
    
      // Extract claims about the user returned by the Gateway. The Microsoft
      // Authentication Library will already have verified these using the public
      // key associated with the API Gateway.
      const { idTokenClaims, accessToken } = authenticationResult
      console.log("Successfully got token from Gateway. ID token claims:\n")
      console.log(JSON.stringify(idTokenClaims, null, 2))
      console.log("\n")
    
      // The id token should contain the CRSid of the user.
      const [crsid, namespace] = (idTokenClaims as { sub: string; }).sub.split("@")
    
      // The API Gateway can authenticate a great number of identities. Make sure
      // this is a CRSid based one.
      if(namespace !== "v1.person.identifiers.cam.ac.uk") {
        throw new Error("Some user without a CRSid authenticated.")
      }
    
      // Fetch the profile of the authenticated user. Note that this can include
      // "private" attributes because we've authenticated as the user. To
      // demonstrate this we'll fetch the "@cam forwarding email", the tombstone
      // email address (for when the user leaves) and any emergency contact
      // information the user may have added.
      const fetchAttributes = [
        "all_identifiers",
        "departingEmail",
        "emergencyContact",
        "forwardingEmail",
      ]
      console.log(`Making a request to Lookup to get profile of ${crsid}...`)
      const personResponse = await fetch(
        `https://api.apps.cam.ac.uk/lookup/v1/person/crsid/${crsid}?fetch=${fetchAttributes.join(",")}`,
        {
          mode: "cors",
          credentials: "omit",
          headers: {
            Authorization: `Bearer ${accessToken}`,
            Accept: "application/json"
          },
        }
      )
    
      // Check response from Lookup.
      if(!personResponse.ok) {
        console.error("Error status:", personResponse.status)
        console.error("Error body:", await personResponse.text())
        throw new Error("Error received from Lookup")
      }
    
      // Log the received profile.
      console.log("Received user profile from Lookup:\n")
      console.log(JSON.stringify(await personResponse.json(), null, 2))
    
      // We want to stop the server now we"ve got a token.
      server.close()
    }
    
    // Wait for server to exit.
    await serverPromise
    
    // vim:sw=2:ts=2:et:sts
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment