DEV Community

Christos Matskas for The 425 Show

Posted on

Open Standards, Security, Azure AD and AWS

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) 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. 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. 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
Enter fullscreen mode Exit fullscreen mode

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"
    ]
}
Enter fullscreen mode Exit fullscreen mode

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}!`))
Enter fullscreen mode Exit fullscreen mode

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 :

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

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. 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

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 ) that worked on this library for their openness to work with us to make the library truly interoperable. Here's to more collaborations!

Discussion (2)

Collapse
094459 profile image
Ricardo Sueiras

Nice!

Collapse
christosmatskas profile image
Christos Matskas Author

Thanks for the kind comment Ricardo and happy coding :)