A simple invite user flow for AAD B2C (without custom policies)
Azure Active Directory B2C (AADB2C) is a good choice for Azure-hosted SaaS applications as it's a) cheap and b) familiar territory if you've done anything with AAD before.
It's pretty easy to get up and running with simple sign in/sign up, but as soon as you want to start doing something slightly more advanced, things start to get tricky quite quickly. Microsoft themselves recommend that you should use the built-in "user flows" for most scenarios and only consider customisation for addressing complex cases (i.e when you don't have a choice).
One example of this is the "invite user" flow - a fairly common feature that's generally required for any business-oriented SaaS application - i.e. you sign up on behalf of your organisation, and then want to invite your colleagues/teammates into the newly created workspace/account/organisation. This isn't supported in the built-in user flows, so you're in the "complex scenario" territory and forced to look at custom policies if you want to implement this feature inside AAD B2C.
We desperately wanted to avoid going down the custom policy route (more on the reasoning behind this later) when adding B2C authentication to our SaaS platform, so we set about designing a simpler approach to the Invite User flow that doesn't require custom policies. This post explains the approach that we took, the reasoning behind it, and the benefits that it gives us.
Before we look at our approach, it's worth being reminded of what we're trying to avoid. Our initial research into implementing Invite User in AAD B2C suggested that there were two main schools of thought on how to tackle it:
Pre-create an account in AAD B2C for the user you want to invite. Tell them to sign in and ask (or force) them to reset their password.
Using Custom Policies
The first option is entirely based around custom policies, using a sample that Microsoft have provided. The sample describes everything you need, and includes a .NET core app to initiate the invite and deal with creating and validating JWT tokens, which are used to package the email address being invited.
Custom policies aren't necessarily "bad" - they're available for a reason, but they're certainly a step change in complexity from using the built-in, configurable user flows. In Microsoft's own words, the target users for the built-in user flows are "All application developers with or without identity expertise", whereas custom policies are aimed at "Identity pros, systems integrators, consultants, and in-house identity teams. They are comfortable with OpenID Connect flows and understand identity providers and claims-based authentication."
The other big difference in approaches is that the built-in user flows can be configured via the "Azure portal with a user-friendly user-interface", v.s. "Directly editing XML files and then uploading to the Azure portal" for custom policies.
So, to summarise - custom policies require a deeper understanding of identity providers and associated concepts, they're harder to work with, and any customisations you make need to be tested appropriately. Essentially, you're on your own in a brave new world of identity.
The second option seems straightforward, but has some immediate limitations. If you're pre-creating the account, you're assuming that you know what user id (i.e. email address) the user wants to use to authenticate with your application. This may be fine in a corporate scenario where you want to mandate that users use their organisational email addresses, but in a B2C scenario you probably need to allow more flexibility.
For example, you might know that you want to invite email@example.com, but you don't know whether they prefer to authenticate with this email/password or a third-party identity provider (i.e. Google or Facebook account), which might be linked to a different email address.
Secondly, creating the account means setting an initial password, which you're going to have to provide to the user and ask them to reset it. If you want to force this reset, this means customising the user flow and you're back in the land of custom policies. So, the easy route has some security risks, and the only way to mitigate that is to use custom policies.
So - neither of these approaches particularly appealed to us. We wanted to keep things as simple as possible when it came to authentication - rely on tried and tested routes for sign in/sign up with minimal customisation. If we had to develop anything bespoke, we'd rather be doing that inside our application rather than inside AAD B2C, so that we had total flexibility over functionality, simplicity and testability.
The approach we adopted was straightforward and was based around us managing and storing our own invite codes. Given that we were already building a multi-tenanted application with persistent storage, this didn't require any fundamental changes in design or thinking about the application or architecture, and allowed us to design a process that worked for us.
This meant we could keep the same AAD B2C user flows for anyone signing in or signing up to our application - and using the built-in user flows meant we didn't need to deal with custom policies. There are two parts to the process - creating the invite and accepting the invite, both of which are described below:
- User A signs up to the application using the built-in AAD B2C Sign Up Flow. This could use the pop-up or redirect auth mechanism.
- User A configures a new organisational account the in the application (e.g. picks account name, handles billing info etc).
- User A generates new invite within the application. Application-specific business rules can be applied here - do they have permission to create invites, does their price plan allow it etc. Invites are just stored as application data against the organisation/tenant.
- User A sends the invite to a colleague/teammate. In the simplest form, this could just be manually emailing a code or a link to someone. Or, it could also be sent automatically to the intended recipient via an application triggered email.
- User B receives invite to join organisation via email. The email contains a link that points the user to the right place for accepting the invite.
- User B clicks on the link (which requires authentication) and signs up to application using the built-in AAD B2C Sign Up Flow. This could use the pop-up or redirect auth mechanism.
- User B redeems the invite to join existing organisation. Application-specific business rules can be applied here - is the invite valid, has it expired, is it associated with a specific email address.
Benefits and considerations
As you can see, the approach above is simple and based entirely around the decision to deal with the invite logic as an application concern, rather than something that should be built into AAD B2C. This avoids the need for AAD B2C customisation, and also opens up the opportunity for additional flexibility inside the application.
For example, whilst the invite has been sent to a specific email address, we can allow the user to sign up to the application using any mechanism that our application supports. All they need is a valid invite - we don't have to restrict it to the recipient's email address (although, of course we can still do that if required).
Additionally, handling the invite flows outside of AAD B2C gives us total flexibility on the associated user experience. The sign up/sign in flow with AAD B2C remains the same for everyone, but everything else outside of that is pure application logic and user experience, meaning we can tailor the flow to our application scenarios.
However, it's also worth highlighting that handling the invite logic ourselves means dealing with all aspects of the requirements, including non-functional concerns. For example,
- What information does the invite contain - do you need to include the organisation it relates to, do you tie to a specific email address? How much information do you need to bake into the invite to ensure it's valid?
- Where are the invites stored - in a multi-tenanted application should you store the invites against the specific tenant to which they relate, or are the invites stored centrally? This is related to the previous point - how do you associate an invite to an organisation/tenant?
- How do you validate an invite - does it have an expiry date? Has it already been redeemed? Is it tied to a specific email address?
- How do you prevent against brute force attacks - can your invites be easily constructed? What happens if someone obtains or generates an invite maliciously?
The answers to all of these are "it depends", relating to your specific application scenarios and requirements. However, removing the complexity/constraints of working within and around the AAD B2C custom policy can simplify the implementation - as long as you accept responsibility for owning the security considerations in relation to your application.
This post has shown that it's possible to design an Invite User flow around AAD B2C that doesn't require custom polices, meaning you have total flexibility over the implementation. This keeps authentication (and the associated AAD B2C user flows) that your application needs simple, and allows you to implement a solution that's as simple or complicated as you require.