API Gateway example
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.
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
Please register or sign in to comment