Getting rid of credentials in Azure – Part 2 (EasyAuth)

This post has been republished via RSS; it originally appeared at: Azure Developer Community Blog articles.

This is part two in a series on how to get rid of credentials wherever you can in Azure. The first part set the stage with configuring "federated credentials" for doing deployments based on Bicep code stored in GitHub using GitHub Actions. The samples in this post assumes you have this in place, so you might want to take a look at that if you haven't:

https://techcommunity.microsoft.com/t5/azure-developer-community-blog/getting-rid-of-credentials-in-azure-part-1/ba-p/3265205

 

Starting with this post we will deploy actual resources in Azure so let's start with something easy in the world of credentialless in Azure - like EasyAuth :)

 

Bad puns aside; EasyAuth is a mechanism that allows you to add identity to Azure App Services in a fairly simple manner. And in line with the topic of the series it is without messing around with credentials of course. (They are present behind the scenes - more on that later.)

 

"How to enable a user to sign in to my webpage" is a question with more possible answers than you would initially think. Is it just the classic typing in of username and password and being able to greet the user with a personalized hello afterwards, or more complex use cases where upon signing in APIs are called on the backend either on behalf of the user or with the app acting with an identity of its own? We are not able to dive into all the finer details of the larger authentication and authorization puzzle here, but we are able to look into some of the basics.

 

Azure App Service is one of several ways to run web apps in Azure. It comes in two "versions" - container-based deployments and directly on an underlying web/app server. It's well-suited for some use cases, and less for others, but those considerations are outside the scope of this post which makes the assumption that an App Service is what you want/need. (We will cover other runtime platforms later.) The instructions here make use of containers, but should work in "plain mode" as well since EasyAuth relies on the hosting platform and not the runtime. While the custom containers are based on C# the features demonstrated are not language dependent.

 

Visual Studio has templates and wizards included that will help you with wiring in the necessary code and libraries, and can even help you generate a so-called app registration in Azure and use the corresponding attributes directly in your code. If you choose the template for a simple web app and Azure AD as the identity provider the file appsettings.json will contain your unique values and look something like this:

 

 

 

{ /* The following identity settings need to be configured before the project can be successfully executed. For more info see https://aka.ms/dotnet-template-ms-identity-platform */ "AzureAd": { "Authority": "https://login.microsoftonline.com/2222-2222-2222-2222", "ClientId": "1111-1111-1111-1111", "ValidateAuthority": true } }

 

 

 

The rest of the code is not relevant right now so just humor me here even if this is not testable. These settings are not a full set of credentials and not secret as such. If I have misconfigured my app registration in Azure AD it could be I'm letting in more users than I'd like too, but without a clientSecret in addition to the clientId you can't directly connect to my data. However, when we host our web app in Azure App Services we can get rid of these values as well. Which is good for two reasons:

  • We don't need to concern ourselves with credentials :)
  • We get code that is so generic that you could fork it from my GitHub and deploy it straight into your App Service without touching the code.

 

Microsoft provides documentation for how to configure EasyAuth in the portal:
https://docs.microsoft.com/en-us/azure/app-service/scenario-secure-app-authentication-app-service

 

Historically EasyAuth was sort of useless for everything but the most basic scenarios as the token you acquired could not be used for anything else than the signin process. With more recent improvements you can actually use the token to call into things like the Graph API as well with an extra configuration step:
https://docs.microsoft.com/en-us/azure/app-service/scenario-secure-app-access-microsoft-graph-as-user#configure-app-service-to-return-a-usable-access-token

 

Deploying a stock container image

We will however start with the more basic things. We will use a sample container from Microsoft, spin up a web site, and enable authentication without ever touching appsettings.json.

 

You can of course do this in the Azure Portal, but in keeping with IaC principles we have Bicep for this as well. Unfortunately Bicep (like ARM) isn't all that great when it comes to registering apps in Azure AD, so you need to run a few lines of Azure CLI first:

 

 

 

$appName = 'foo' # WebApps require a replyUrl - this is a known suffix for EasyAuth $replyUrl = "https://$appname.azurewebsites.net/.auth/login/aad/callback" # Create an app registration $appId = (az ad app create --display-name $appName --reply-urls $replyUrl --query appId) # Create a service principal for the app registration az ad sp create --id $appId # Add permission for User.Read on MS Graph (static guids) az ad app permission add --id $appId --api 00000003-0000-0000-c000-000000000000 --api-permissions 311a71cc-e848-46a1-bdf8-97ff7156d8e6=Scope # Grant the permission added above az ad app permission grant --id $appId --api 00000003-0000-0000-c000-000000000000 # Deploy the Bicep template az deployment sub create -l $location --name ExampleDeployment --template-file main.bicep --parameters azuredeploy.Dev.parameters.json env=Dev authClientId=$appId appName=$appName

 

 

 

Link: https://github.com/ahelland/Bicep-Landing-Zones/blob/main/credless-in-azure-samples/part-2/easyauth-stock-image/aad-app-reg.azcli

 

For the IaC nitpickers out there - yes, you can technically put this in a deploymentScript element in Bicep. Right now I recommend you run the cli script locally (both app registration and deploying the template) so it isn't required to make things more complicated than necessary.

 

The settings for EasyAuth are embedded as a child resource of the app service:

 

 

 

resource appservice 'Microsoft.Web/sites@2021-03-01' = { name: name location: location tags: resourceTags properties: { siteConfig: { appSettings: [ { name: 'DOCKER_REGISTRY_SERVER_URL' value: dockerRegistryUrl } { name: 'DOCKER_REGISTRY_SERVER_USERNAME' value: dockerRegistryUsername } { name: 'DOCKER_REGISTRY_SERVER_PASSWORD' value: dockerRegistryPassword } { name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE' value: 'false' } ] linuxFxVersion: linuxFxVersion appCommandLine: dockerRegistryStartupCommand alwaysOn: alwaysOn http20Enabled: true } serverFarmId: '/subscriptions/${subscriptionId}/resourcegroups/${serverFarmResourceGroup}/providers/Microsoft.Web/serverfarms/${appServicePlanName}' clientAffinityEnabled: false httpsOnly: true } dependsOn: [] resource easyauth_config 'config' = { name: 'authsettingsV2' properties: { httpSettings: { requireHttps: true } globalValidation: { requireAuthentication: true redirectToProvider: 'azureActiveDirectory' unauthenticatedClientAction: 'RedirectToLoginPage' } platform: { enabled: easyauthEnabled } login: { tokenStore: { enabled: true } } identityProviders: { azureActiveDirectory: { enabled: aadProviderEnabled registration: { clientId: authClientId } } } } } }

 

 

 

Link: https://github.com/ahelland/Bicep-Landing-Zones/blob/main/credless-in-azure-samples/part-2/easyauth-stock-image/app-service.bicep

 

In this case we used azureActiveDirectory (called "Microsoft" in the portal), but you could use Facebook, Google, etc. if you so like in a similar fashion:

https://docs.microsoft.com/en-us/azure/templates/microsoft.web/sites/config-authsettingsv2?tabs=bicep

 

The quickest way to deploy this is to use az cli locally (in the

Bicep-Landing-Zones/credless-in-azure-samples/part-2/easyauth-stock-image

 folder) where authClientId => the appId you generated in the previous step & appName => the unique part left of .azurewebsites.net:

 

 

 

az deployment sub create -l norwayeast --name ExampleDeployment --template-file main.bicep --parameters azuredeploy.Dev.parameters.json env=Dev authClientId=appid appName=foobar

 

 

 

 

This will deploy a standard container provided by Microsoft with a "Hello World"-type web app and consist of code not touched by me in any way while still requiring the user to sign in.

EasyAuth-01.png

 

This is nice.

 

Deploying a claims reader app

Ok, so clearly the Azure platform is able to do some magic outside of your application code. But since the code here clearly has no concept of identity - what if we want that "Hello Andreas!" greeting? That would require code to handle claims in some way, right? Yes, it would.

 

Let's demo this in two ways. The first is swapping out this with a container image called blazor-easyauth-default-dotnet6 that I have prepared.

 

I ran through the wizard for creating a Blazor serverside app, and on the index page I included code to retrieve and print out the claims of the user identity to demonstrate we have access to those bits as well:

 

 

 

@page "/" @using System.Security.Claims @using Microsoft.AspNetCore.Components.Authorization @inject AuthenticationStateProvider AuthenticationStateProvider <PageTitle>Index</PageTitle> <h2>Claims</h2> <button @onclick="GetClaims">Get claims of currently logged in user.</button> @if (claims.Count() > 0) { <table class="table"> <thead> <tr> <th scope="col">Claim Type</th> <th scope="col">Claim Value</th> </tr> </thead> <tbody> @foreach (var claim in claims) { <tr> <td>@claim.Type</td> <td>@claim.Value</td> </tr> } </tbody> </table> } @code { //For handling the claims in the session private string authStatusMsg = string.Empty; private IEnumerable<Claim> claims = Enumerable.Empty<Claim>(); private async Task GetClaims() { var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var user = authState.User; if (user.Identity.IsAuthenticated) { authStatusMsg = $"{user.Identity.Name} has been authenticated."; claims = user.Claims; } else { authStatusMsg = $"User has not been authenticated."; } } }

 

 

 

Link: https://github.com/ahelland/Identity-CodeSamples-v2/blob/master/blazor-easyauth-default-dotnet6/Pages/Index.razor

 

This can be deployed with the Bicep code here (which will be explained in a moment):
https://github.com/ahelland/Bicep-Landing-Zones/blob/main/.github/workflows/DeployCredlessEADefault.yml

EasyAuth-02.png

 

Deploying an app interacting with Microsoft Graph

The second sample takes a cue from another article in the official docs on how to use the Microsoft Graph with EasyAuth:

https://docs.microsoft.com/en-us/azure/app-service/scenario-secure-app-access-microsoft-graph-as-user

 

This requires a few additions in the code. In appsettings.json we still leave the AzureAd elements with the default values, but we add the settings required for MS Graph:

 

 

 

"Graph": { "BaseUrl": "https://graph.microsoft.com/v1.0", "Scopes": "user.read" }, ´´´ In Program.cs we also add a little bit to the setup: ´´´ // Add services to the container. builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi() .AddMicrosoftGraph(builder.Configuration.GetSection("Graph")) .AddInMemoryTokenCaches();

 

 

 

And in Index.razor we need to add some code and markup to do the actual call to the Graph and display the properties accordingly:

 

 

 

@page "/" @using Microsoft.Graph @using Microsoft.AspNetCore.Authorization @inject GraphServiceClient GraphClient <PageTitle>Index</PageTitle> <h2>User properties from MS Graph</h2> @if (graphUser != null) { <p>Hello @graphUser.DisplayName, it looks like your phone number is @graphUser.MobilePhone</p> } <p> You should be seeing your name and mobile phone number above if the call to MS Graph worked. (Mobile phone only works if it is defined on the user object.) </p> @code { protected User graphUser; protected override async Task OnInitializedAsync() { graphUser = await GraphClient.Me.Request().GetAsync(); } }

 

 

 

You can get all the code from here:

https://github.com/ahelland/Identity-CodeSamples-v2/tree/master/blazor-easyauth-graph-dotnet6

 

And the deployment Action is here:

https://github.com/ahelland/Bicep-Landing-Zones/blob/main/.github/workflows/DeployCredlessEAGraph.yml

 

Loading up this code in your browser will yield something like this:

EasyAuth-03.png

(I have not defined a phone number for the user object in this test environment hence the blank value, but the name is correctly retrieved so it's still working as intended from a code perspective.)

 

The trick (for both versions) is essentially that EasyAuth not only exposes a preconfigured callback endpoint, but also a fixed endpoint for tokens at /.auth/me that the app can call into.

 

I skipped explaining both the GitHub Action and the Bicep code so let's do that now. The difference between these two images is basically just the name of the image so they are the same thing. I extrapolated this to a generic Bicep module that can be re-used as seem fit.

 

Compared to deploying the stock image we need to expand the EasyAuth configuration in Bicep:

 

 

 

identityProviders: { azureActiveDirectory: { enabled: aadProviderEnabled registration: { clientId: authClientId openIdIssuer: 'https://${aadEndpoint}/${tenantId}/v2.0/' clientSecretSettingName: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET' } login: { loginParameters: [ 'response_type=code id_token' 'scope=openid offline_access profile https://graph.microsoft.com/User.Read' ] } } }

 

 

 

The loginParameters section is what we referred to earlier as "getting a usable token". This in turn requires that we provide the openIdIssuer parameter to validate the issuer of said token, and we actually also need a clientSecret to make this work. (As I said - some things are still present behind the scenes.)

 

As for the GitHub Action that makes this tick. That was a mouthful to get working correctly.   We need to do this in a multi-step process:

  • Create an Azure AD app registration with placeholder settings.
  • Generate a client secret for the registered app.
  • Deploy the Azure App Service. (Will not work after initial deployment.)
  • Retrieve the name of the App Service with a generated suffix element for pseudo-randomness.
  • Update the App Registration with the correct replyUrl based on previous step.

 

It could have been made easier without the suffix element for the app name, but that was included to make it easier to deploy with unique names (which are required). In real-life you would put a custom domain name in front anyways.

 

Lesson learned here is that putting this in one long script is not how GitHub likes its Actions so in total it ends up as a fair number of lines of yaml when you also include the validation steps from before.

 

If you poke around in the portal and the deployment logs in GitHub you might notice two things.

 

As indicated by the Bicep above you have a configuration setting called MICROSOFT_PROVIDER_AUTHENTICATION_SECRET, and this can be viewed in the Azure Portal:

EasyAuth-04.png

 

And if you take a look in the logs you can see this in clear text as well:

EasyAuth-05.png

 

 

Doesn't this violate my proposed intent of getting rid of credentials? The default consultant style answer would of course be "it depends" :) Your code is clean and contains no credentials so we have improved upon things in general. However, no we don't want these things to be as easily viewable. (Even if they are not readable for the entire internet.)

 

As I've already said, we're not able to get rid of all secrets, and this is the way Azure App Services work. The recommended approach is that you put the secret in Azure Key Vault, and just include a reference in the App Service configuration. To make this work we need to look into Managed Identity for the web app. We haven't gotten around to that yet, but that will be on the agenda for the next post of the series :smile:

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.