Open Standards, Security, Azure AD and AWS
Published: 2021-11-05 01:45:29

One of the things we always talk about as Microsoft Identity Dev Advocates, is the openness and interoperability of our platform. You'll often hear me say: "any platform, any language" and that extends to tools and libraries.

For example, in the past 12 months, we worked closely with the JetBrain team to bring Azure AD integration in the Rider and IntelliJ IDEs to ensure that developers can successfully implement authentication and authorization in their applications directly from their favorite IDE. 

Anything that can or should work with Azure AD is a fair game for me. So you can imagine how excited I got when I saw the awesome new library ( [aws-jwt-verify](https://github.com/awslabs/aws-jwt-verify )) from the AWS that could perform token validation with 1st and 3rd party Identity Providers. So I got to work and built a Node.js app to test the library...

Unfortunately, my first attempt to build a working sample failed because there was a small issue with the library. The `alg` claim was expected in the JWT header but Azure AD omits it because it's marked is an optional field as per [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.4). Sinc the library is open source, we jumped to the repo to let the team know. I also gave them a polite nudge on Twitter to ensure that we got some eyeballs.

The team was quick to respond and within a few days we got a fix! The library works flawlessly now with Azure AD so if you're building an application that needs to verify (Azure AD) tokens, now you have a new library to work with. 

## Create the Azure AD App Registrations

Since this example builds on a previous blog post, check out the **Create the Azure AD App Registrations** section [here](https://dev.to/425show/secure-apis-using-node-js-azure-ad-cosmos-db-and-the-azure-sdks-4e4i). At some point I'll provide you with a Python/.NET Notebook to do this programmatically too :)

## Build the secure Node API
Open your favorite terminal and type the following:
```
mkdir <your-project-name>
cd <your-project-name>
npm init -y
npm install aws-jwt-verify
npm install body-parser
npm install express
npm install --save-dev @types/node
```
Create 2 files: 
- index.ts
- tsconfig.json

Open the project in code with `code .` and edit the `tsconfig.json` file to add the following code:

```
{
    "compilerOptions": {
        "target":"esnext",
        "sourceMap": true,
        "module": "esnext",
        "outDir": "out",
        "moduleResolution": "node",
        "allowSyntheticDefaultImports":true
    },
    "exclude": [
        "node_modules"
    ]
}
```
Finally, we're ready to add the code for our API. Open the `index.ts` file and add the following code:

```
import { JwtRsaVerifier } from "aws-jwt-verify";
import express from 'express';
import bodyParser from 'body-parser';

const SERVER_PORT = process.env.PORT || 8000;
const readOnlyScope: Array<string> = ["access_as_reader"];
const readWriteScope: Array<string> = ["access_as_writer"];
let accessToken;

const config = {
    auth: {
        clientId: "c7639087-cb59-4011-88ed-5d535bafc525",
        authority: "https://login.microsoftonline.com/e801a3ad-3690-4aa0-a142-1d77cb360b07",
        jwtKeyDiscoveryEndpoint: "https://login.microsoftonline.com/common/discovery/keys"
    }
};

const verifier = JwtRsaVerifier.create({
    tokenUse: "access",
    issuer: `${config.auth.authority}/v2.0`,
    audience: config.auth.clientId,
    jwksUri: config.auth.jwtKeyDiscoveryEndpoint
  });

const validateJwt = async (req, res, next) => {
    const authHeader = req.headers.authorization;
    if (authHeader) {
        const token = authHeader.split(' ')[1];

        try {
            const payload = await verifier.verify(token);
            console.info("Token is valid.");
            accessToken = payload;
            next();
        } catch {
            console.error("Token not valid!");
            return res.sendStatus(401);
        }
    } else {
        res.sendStatus(401);
    }
};

function confirmRequestHasTheRightScope(scopes:Array<string>): boolean{
    const tokenScopes:Array<string> = accessToken.scp.split(" ");
    scopes.forEach(scope => {
        if(!tokenScopes.includes(scope)){
            return false;
        }
    });
    return true;
}

// Create Express App and Routes
const app = express();
app.use(bodyParser.json());

app.get('/', (req, res)=>{
    var data = {
        "endpoint1": "/read",
        "endpoint2": "/write"
    };
    res.send(data); 
})

app.get('/read', validateJwt, (req, res) => {
    if(!confirmRequestHasTheRightScope(readOnlyScope)){
        res.status(403).send("Missing or invalid scope");
    };
    var data ={
        "message": "Congratulations - you read some data securely"
    }
    res.status(200).send(data);
});

app.get('/write', validateJwt, (req, res) => {
    if(!confirmRequestHasTheRightScope(readWriteScope)){
        res.status(403).send("Missing or invalid scope");
    };
    var payload = JSON.stringify(req.body);
    res.status(200).send(payload);
});

app.listen(SERVER_PORT, () => console.log(`Secure Node API is listening on port ${SERVER_PORT}!`))
```

The API provides 3 endpoints
- `/` (insecure)
- `/read` (secure | requires a read-only scope)
- `/write` (secure | requires a read-write scope)

The `config` object contains the necessary information for the `verifier` to be able to validate the authenticity and validity of the incoming token. Things like the `authority` and `audience` of the token ensuring that the token was issued by the right Azure AD tenant and it's for the right audience (i.e. our API). Without these checks, an attacker could pass any valid Azure AD token to our API.

The instantiation of the `verifier` is extremely simple, although there are [more options](https://github.com/awslabs/aws-jwt-verify#cognitojwtverifier-verify-parameters) :
```
const verifier = JwtRsaVerifier.create({
    tokenUse: "access",
    issuer: `${config.auth.authority}/v2.0`,
    audience: config.auth.clientId,
    jwksUri: config.auth.jwtKeyDiscoveryEndpoint
  });
```
We use the `config` object to populate the necessary options and set the verifier to only accept Access tokens with the `tokenUse` option. Once we instantiate the verifier, we can call the `.verify(token);` method to validate the passed token. I chose to implement a middleware function to do the token validation :)

## Show me the code
You can find the full source code from this blog post on [GitHub](https://github.com/425show/VerifyAzureADTokensWithAWSLib). And if you want a more complex example where we show you how to securely call some Azure Services such as Key Vault, Cosmos DB and Azure Storage, check out this [GitHub repo](https://github.com/cmatskas/SecureNodeAPIWithAAD/tree/aws_jwt_verify)

## Summary
This blog post is a testament of what Open Source and open standards can achieve. The ability to use the language, tools and libraries of your choice to achieve your goals is great and I, for one, am truly excited that I get to do this every day. I also want to give a big shoutout to the AWS dev team (such as [ottokruse](https://twitter.com/ottokruse) ) that worked on this library for their openness to work with us to make the library truly interoperable. Here's to more collaborations!