Or "Getting rid of credentials in Azure Part 5" for those who have seen my previous posts.
For a little while I have been doing this series on getting rid of credentials in Azure (although in retrospect I see that it would probably be more correct to say "getting rid of classic credentials"). Along the way we've covered federated credentials, managed identity, EasyAuth and more. It has been neatly packaged into CI/CD pipelines with GitHub Actions as well for further streamlining. I'm happy with the results so far even though some of it took quite the effort to pull off.
The EasyAuth login process was pretty much as far as we got regarding the user facing side of things as most of the work was behind the scenes making the infra work without the use of passwords and client secrets. And the EasyAuth term is still not something the user cares or knows about. That's the way it had to be though; the logical progression is to harden the platform first before tackling the higher level bits.
We can't leave the user part out now that we've been so hard at it. Let's see if we can make endusers work without passwords too. I'm thinking "magic links". You may have visited websites where the sign-in page has you enter your email address and you then receive a link (via email) that takes you to the authenticated version of the page without typing in a password. This is the experience we will re-create here.
Product wise we will include more Azure components to achieve this. The end user accounts will reside in Azure AD B2C as that gives us the ability to have greater control of the authentication process. I actually wrote an article way back that described how to set up Azure AD B2C this way, but it's no understatement to say it had a lot of manual hoops and associated complexity. I promise this will be more easily digestible material :)
Why not FIDO keys or Windows Hello or something like that? Well, you can actually do that as well on a configuration level. The app used for generating urls is secured with Azure AD B2E and I have configured my lab tenant to allow me using both of these methods. This means I can make this step passwordless as well, but it's not part of my application code in any way. Correspondingly this is an exercise left to the reader.
The way this works from a developer standpoint is that the OpenID Connect standard allows extra parameters in the query url to drive the authentication process and we're making use of the id_token_hint parameter. We package name, email, etc. of the user into a Json Web Token (JWT) making sure to sign it with a certificate. When the user clicks a link with the token embedded Azure AD B2C will parse this JWT (properly validating the token at the same time) and issue new JWTs from the B2C tenant for the actual authentication. (The JWT we create is as the name implies just a hint; not the actual access or id tokens.) To make things easier for demo purposes we skip an actual email integration and just present the correct url for sign-on on a web page. In addition we use the same approach for a sign-up experience where we pre-seed a user object so the user doesn't have to type in this.
A real-life use case (for the sign-up flow) could be something like going into a retail store and buying a product and after they've filled in your name and details needed for warranty you can receive a link for completing the sign-up experience on the retailer's web page at your own leisure without re-entering the info they already have.
We're doing this exercise as a microservice-based solution employing Azure Container Apps. We need three services:
OpenID Connect metadata endpoint exposing the details needed for token validation.
A web app for generating sign-in and sign-up links.
A web app for consuming the token and displaying the claims of the user.
Azure Container Apps
We create a certificate for the signing and store this in a KeyVault (to be accessed by the metadata and link generation microservices).
The source code itself isn't all that interesting, but can be found here:
In total these are the values you need in your GitHub environment secrets config:
GitHub Environment Variables
Since we work with two Azure AD tenants we need a GitHub Action app registration in both, and we need to switch between the two during the execution of the workflow.
There are custom policies for Azure AD B2C that handle how to handle the sign-in and sign-up, and the placeholder values in the templates are replaced during deployment and uploaded.
If you're not familiar with authoring custom policies in XML this isn't an introduction to said topic (Microsoft has docs of course), but the relevant snippet looks like this:
<DisplayName>My ID Token Hint ClaimsProvider</DisplayName>
<DisplayName> My ID Token Hint TechnicalProfile</DisplayName>
<Protocol Name="None" />
<!--Sample: Read the email claim from the id_token_hint-->
<OutputClaim ClaimTypeReferenceId="email" />
Not to worry though. You will of course need to have an Azure AD B2C tenant ready, but the policies are fully automated and shouldn't require any extra input. (I provide default base and extension policies so no need to mess around with that.)
In addition we create the necessary app registrations in Azure AD (one for B2E and one for B2C) to be used in the auth processes.
The infra code is split in two parts - first we create a container registry that is used to push the code into, and after that the Container Apps can be provisioned from this registry.
The eagle-eyed among you might notice that we are wiring password and username for the registry into the provisioning of the apps. This is because the managed identity one can assign to the container apps only apply within the container and not the bootstrapping of the container. I've been told this is on the list of improvements to be made for Azure Container Apps, but no details on when and how that happens. So, it's a workaround for now. (Since the code is open-source we could just as well have put the images in a container registry allowing anonymous pulls as a different take on the issue.)
As in previous posts a lot of the meat of the work goes into the GitHub Action that ties it all together. It's a three phase config:
First create the Azure AD B2C app registration and service principal. (So, we can provision these details into the B2C-integrated app.)
Create "regular" Azure AD app registration for the url generation app. This also takes care of deploying the Bicep files, push the apps, etc.
Deploy custom policies into the B2C tenant. This has to be done after we have the metadata urls, clientIds, etc. that we need.
Note: While it is possible in general to add delegated permissions in a CI/CD pipeline without admin consent this does not work for B2C. If this is a bug, or by design I do not know. (The end-user doesn't interact with B2C the same way as in Azure AD B2E so I can see an argument for why it should be like this.) After running the GitHub Action you need to manually go into the B2C tenant, find the app registration, and click "Grant admin consent for Contoso":
Azure AD B2C Permission Consent
What do you get after this process completes? Well, first step is to visit the url generation page:
SignIn URL Generator
Signing in will only work for accounts that exist in Azure AD B2C, and successfully clicking the link will take you to a page like this
Signed In with Token Hint
What if you don't have an account? Choose sign-up first:
SignUp URL Generator
Which will take you to the SignUp page:
Signing Up with Token Hint
But this asks me to enter a password! Yes, I included that as a fallback option for testing. There's also a more normal out of the box sign up/in policy that have been deployed if you prefer that (B2C_1A_Signup_Signin_GitHub). This is not required though, you can eliminate this through other B2C trickery for a truly passwordless experience.
Like all proof of concepts it's a bit rough around the edges and not a polished experience, but let's not forget what our intent starting out was - get rid of as many passwords and secrets as we can. What we have here is some pieces of C# and Bicep that you can clone from my GitHub, configure with a couple non-secret identifiers and without even trying to spell out P@ssw0rd you get a secure deployment and a passwordless user experience. I'm not forcing anyone in the audience to be impressed, but I think it is a sweet little party trick and decent conclusion to this journey