Contents

OAuth 2.0 Authorization Code Flow with Azure Functions and Microsoft Identity - Part 1: Getting an Access Token

This is part one in a three part series on Authorization Code Flow with Microsoft Identity. You can also follow through to Part 2 and Part 3.

Single Page Applications (SPAs) are a great. For me, they are cheap front-end as they are just static files that don’t need a whole web server/service and there for can be run for basically nothing. I generally run mine on an Azure CDN and it costs me about $0.01 per month. They also allow you to run whatever you want as a backend as well. For me, that is generally Azure Functions.

When you do run a SPA though, authentication gets a little tricky. As all of your code is visible to anyone that knows how to use a browser’s dev tools, secrets are not so secret. So how does one validate their identity when using a Single Page App?

One option is Implicit Grant Flow, which redirects a user to a login prompt, and once signed in, sets a cookie with a short-lived access token. When that token expires (generally after an hour), the user needs to sign in again. Not a great user experience.
Apart from this, modern browsers have killed the usability of implicit flow, as the cookie that sets the access token is on a different domain to yours, and modern browsers are moving to disable access to 3rd-party cookies, so you can’t get the access token and will never be authenticated.

The only other option is Authentication Code Flow, which once you have signed in, returns to a page of your choosing with a one-time authentication code; which your app uses sends to a backend service to validate the code with your authentication provider.

This is what I intend to build in this post. There will be no front-end, it is not needed to show what I want to do.

First, start with an Application Registration

Head on over to https://aad.portal.azure.com and select Azure Active Directory > App registrations. Create a New Registration with the following details:

  • Name: Whatever you like
  • Supported Account Types: Accounts in this organizational directory only
  • Redirect URI: [Web] https://localhost/auth

/media/2021/04/image.png

Note that I am suggesting we use a single-tenant app here, and not a multitenant app. This is purely for simplicity as multitenant apps add additional complexity that takes us away from the point of this article. I have, however, noted what you need to change to make this work for multitenant apps further down.

Once created, on the Overview page take note of the Application (client) ID, then head to Certificates & secrets and create a new Client secret, taking note of what that secret is as it will not be available to you after you have left the page.

/media/2021/04/image-2.png

/media/2021/04/image-1.png

Later, we will make a call to the Microsoft Identity platform and request an authorization token, which will then be used on our Functions back-end to authenticate

Now let’s make the Azure Function

In Visual Studio, create a new Azure Functions Project with no Function, we’ll add a function in later.

In this, we will accept a body with the authorization token, and then ask Microsoft Identity Platform for an access token to send back to the front-end.

Open up the local.settings.json file and add the TenantId, ClientId, and ClientSecret settings, populating them with the values I told you to take note of earlier. The TenantId can be the domain of your tenant, your onmicrosoft.com domain, or a GUID identifying the tenant.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "TenantId": "yourtenantdomain.com",
    "ClientId": "The application Client Id",
    "ClientSecret": "The Client Secret you created earlier"
  }
}

Now create the following class, which handle the calls to the Microsoft Identity Service

public class MicrosoftIdentityClient
{
    private static readonly HttpClient httpClient = new HttpClient();
    private static readonly string hostUrl = "https://login.microsoftonline.com";

    private readonly string tenantId;
    private readonly string clientId;
    private readonly string clientSecret;

    public MicrosoftIdentityClient(string clientId, string clientSecret, string tenantId)
    {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.tenantId = tenantId;
    }
}

Now add in the method to get the access token using the authorization code

public async Task<string> GetAccessTokenFromAuthorizationCode(string authCode)
{
    string redirectUrl = "https://localhost/auth";
    string scopes = "openid offline_access https://graph.microsoft.com/user.read";

    Uri requestUri = new Uri($"{hostUrl}/{this.tenantId}/oauth2/v2.0/token");

    List<KeyValuePair<string, string>> content = new List<KeyValuePair<string, string>>()
    {
        new KeyValuePair<string, string>("client_id", this.clientId),
        new KeyValuePair<string, string>("scope", scopes),
        new KeyValuePair<string, string>("grant_type", "authorization_code"),
        new KeyValuePair<string, string>("code", authCode),
        new KeyValuePair<string, string>("redirect_uri", redirectUrl),
        new KeyValuePair<string, string>("client_secret", this.clientSecret)
    };

    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, requestUri)
    {
        Content = new FormUrlEncodedContent(content),
    };

    HttpResponseMessage response = await httpClient.SendAsync(request);

    string responseContent = await response.Content.ReadAsStringAsync();
    dynamic responseObject = JsonConvert.DeserializeObject(responseContent);

    if (response.IsSuccessStatusCode)
    {
        // dynamic values need to be assigned before passing back
        return (string)responseObject.access_token;
    }
    else if (response.StatusCode == HttpStatusCode.BadRequest)
    {
        // Something failed along the way, and there will be an error in there if the error code is 400
        // Handle it however you want.
        throw new Exception((string)responseObject.error_description);
    }
    else
    {
        // ¯\_(ツ)_/¯
        throw new Exception("Something bad happened");
    }
}

Now it is time to add the HTTP Trigger Function, which you can do from the solution explorer by right-clicking on the project, and selecting Add > New Azure Function. Give it a name, and choose HTTP Trigger with an Anonymous authorization level.

/media/2021/04/image-9.png

Replace the function call with the below, this will grab an authentication code and use the class made above to call Microsoft Identity to return the access token.

[FunctionName("AuthHttpTrigger")]
public static async Task<IActionResult> Run(
  [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
  ILogger log)
{

  // Get the authentication code from the request payload
  string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
  dynamic data = JsonConvert.DeserializeObject(requestBody);
  string authCode = data.authCode;

  // Get the Application details from the settings
  string tenantId = Environment.GetEnvironmentVariable("TenantId", EnvironmentVariableTarget.Process);
  string clientId = Environment.GetEnvironmentVariable("ClientId", EnvironmentVariableTarget.Process);
  string clientSecret = Environment.GetEnvironmentVariable("ClientSecret", EnvironmentVariableTarget.Process);

  // Get the access token from MS Identity
  MicrosoftIdentityClient idClient = new MicrosoftIdentityClient(clientId, clientSecret, tenantId);
  string accessToken = await idClient.GetAccessTokenFromAuthorizationCode(authCode);

  return new OkObjectResult(accessToken);
}

Now you can spin up the functions host locally, which will use the settings from your local.settings.json file, and we can see this through.

/media/2021/04/image-11.png

Let’s get the authentication code

Open up a browser, I’d recommend in incognito/in-private mode. We now need to build up a specific URL to call MS Identity and authenticate. All of the below should be on one line, but has been broken over multiple lines so it is easier to read

https://login.microsoftonline.com/<Tenant ID>/oauth2/v2.0/authorize
    ?client_id=<Client ID>
    &response_type=code
    &redirect_uri=<Redirect URL>
    &response_mode=fragment
    &scope=openid%20offline_access%20https%3A%2F%2Fgraph.microsoft.com%2Fuser.read
    &state=12345

Here, where should be replaced with the GUID Client Id that you have noted down earlier, and the replaced with the URI added when registering the application. If you have not adjusted the Application Scopes in the AzureAD portal, then you can leave the scope value as it is, but you are welcome to adjust it.

If you get the URL correct, you will be prompted to sign in and consent to the scopes mentioned in the URL.

/media/2021/04/image-6.png

Once you have signed in and consented, your browser will say that you could not connect.

/media/2021/04/image-5.png

That’s because the redirect URL was pointing to localhost, and you don’t have any localhost running. But the magic is in the address bar. Here you need to copy and put aside the value of the code from the query.

/media/2021/04/image-7.png

Don’t copy everything though, as there are other values in the query.

/media/2021/04/image-8.png

Now for the access token

Fire up Postman (or your favourite equivalent) and make a POST call to your function backend. The response payload should be in the following JSON format:

{
    "authCode": "<Put your authentication code here>"
}

/media/2021/04/image-12.png

If everything works out, you should get an access token in the response body.

/media/2021/04/image-13.png

If you get a 500, you can debug and see what went wrong, but the most common causes are:

  • You only have a very short window to use an authentication code, so it may have expired
  • You are only allowed to use an authentication code once, so you may need to call the authorize URL again in your browser and extract a new code

Let’s give it a test

Seeing as you are in Postman (or your favourite equivalent), let’s make a call to the graph. This is assuming you didn’t change the scopes when making the app registration.

Make a GET request to https://graph.microsoft.com/v1.0/me and use the returned token from the previous call as the Bearer token. You should see data returned looking like the image below.

/media/2021/04/image-14.png

You should get a response with information about yourself! Hooray!

/media/2021/04/image-15.png?w=1024

Is that it?

Now that you have gone through all that to get an access token back to your front-end, how is that any different from implicit flow? Well, apart from not needing to access 3rd party cookies the end result is not much different. Your front-end now has a short-lived token that you will need to get again when it expires, which is generally around an hour. Even if the user is signed in, your app will need to redirect away from what it is doing to authenticate, and then return with the authentication code.

In my next post, I will discuss how to make use of refresh tokens and OpenID Connect ID tokens to avoid needing to sign in and get a new authentication code when the access token has expired.

Making this work for Multi-Tenant Apps

All this shows you how to do authentication code flow on a Single-Tenant App Registration, but what if you wanted to make this for a multi-tenant app registration, and allow users from outside your org’s tenant?

I did mention that there is some extra complexity to this, and it is around unverified publishers. Previously Microsoft made the decision that multi-tenant applications cannot be consented to (the screen where it asked if you allow certain permissions) unless the publisher of the application is registered as a Microsoft Partner and they have gone through a series of verification steps. There is only one way to avoid this, having a tenant administrator consent to the app on behalf of the organization.

The below steps will describe what you need to do to change the above app to multi-tenant and get administrator consent for the tenant.

Firstly, go back to the AzureAD portal (https://aad.portal.azure.com/) and open your Application registration, then select Authentication and switch the Supported Account Types to Accounts in any organizational directory (Any Azure AD directory - Multitenant) and don’t forget to save it.

Next, go back into your function app and change the tenantId to not come from settings, but just be the value “common”

// Get the Application details from the settings
- string tenantId = Environment.GetEnvironmentVariable("TenantId", EnvironmentVariableTarget.Process);
+ string tenantId = "common";
string clientId = Environment.GetEnvironmentVariable("ClientId", EnvironmentVariableTarget.Process);
string clientSecret = Environment.GetEnvironmentVariable("ClientSecret", EnvironmentVariableTarget.Process);

Lastly, for any tenant that is to use the app, an administrator for that tenant needs to go to a specific URL in their browser, sign-in, and consent on the behalf of the tenant. The URL needs to be structured as shown and like above, has been broken over multiple lines so it is easier to read.

https://login.microsoftonline.com/organizations/v2.0/adminconsent
    ?client_id=<Client ID>
    &state=12345
    &redirect_uri=<Redirect URL>
    &scope=openid%20offline_access%20https%3A%2F%2Fgraph.microsoft.com%2Fuser.read

Again, client_id should be replaced with the GUID Client Id that you have noted down earlier, and the Redirect URI replaced with the URI added when registering the application.

At this point, the tenant admin should see something like this, where they can consent for the organization.

/media/2021/04/image-16.png

From then on in, you should be right to use as a multitenant app. This method is very limiting though if you do want to host across multiple tenants, and if you did want to do this at scale I’d suggest you get yourself verified as a publisher.