Most web applications nowadays need some authentication mechanism to differentiate their users for various levels of personalization. However, user authentication tends to get more sophisticated due to the variety of client platforms as well as the susceptibility to attacks.
As a user, I no longer want to create a new credential for each site I am visiting. Giving my password to a site requires a certain amount of trust in the site’s security since not all sites implements proper amount of protection to password (or credential in general) with standard mechanisms of password hashing or 2FA.
As a system developer, protecting users’ passwords and users’ credentials in general becomes a big burden due to the growing list of attack techniques. This is also a source of distraction when signing in is certainly not the focus of most web application.
A better way to tackle authentication would be to leave this part to the “professional,” or in other words, to delegate user authentication to popular identity services such as Google Account or Microsoft Account. Our web application now only needs to take care of authorization and managing users’ data. This has become a major trend thanks to the introduction of JWT and OIDC protocol.
In this post, I’ll go through what I have done to incorporate Signing in with Microsoft Account into my ASP.NET 5 web applications. This should be directly applicable for ASP.NET Core 3 thanks to similar authentication setup. Authentication mechanism in this post is using open standards such as JWT and OIDC, so it should be able to extend to many other identity providers such as Google Account.
Design Goals
- The web application must not handle authentication. Particularly, it must not store any passwords or user credentials.
- The web application must be able to handle authorization. This means the app needs to be able to get identity information from the identity provider, specifically user identity (such as user ID) and some role information (if any).
- On mobile apps or other native clients (such as WPF on Windows), identity/session information can be in a JWT stored in local storage. However, in web apps (including SPA sites), the preferred storage mechanism would be with cookies in order to leverage more native browser features and less reliance on JavaScript.
- In web browsers, all web requests are automatically attached with the authentication cookie by the browsers themselves, so no JavaScript is required to attached additional headers (for both MVC web app and more modern SPA).
- In native apps (mobile or PC), authentication header will need to be attached to each HTTP request by application code.
- Authentication flow must be handled by codes within ASP.NET itself or extra libraries from Microsoft. This is to prevent mis-implementing aspects of the authentication.
Implementation
My implementation uses libraries from Microsoft, most of them are in ASP.NET 5 (and also in ASP.NET Core 3).
To make the implementation as simplistic as possible, ASP.NET Core Identity is not used. The implementation will only show that you can get identity information inside ClaimsPrincipal. You will have to add your own code to create user profiles from the received identity.
As a bonus, this implementation also handles authentication and authorization for a GraphQL endpoint using HotChocolate. This might look simple as HotChocolate has support for its own AuthorizeAttribute, but there is some complication over multiple authentication schemes.
Azure AD Setting Up
- Open Azure portal and then App registrations
- Click New registration and fill in the details.
Choose account types as needed for your application; for this post you should choose the last option for both AD and MSA.
Redirect URI can be filled in later. - After the application is created, in tab Overview, copy Application (client) ID to fill in the appropriate setting in web and WPF client in the ASP.NET Code below.
- In tab Authentication, add Web platform (if not exists) and fill in Redirect URIs such as https://localhost:44364/signin-microsoft. This also depends on the port you use in the ASP.NET Code.
- Add Mobile and desktop applications and add Redirect URIs http://localhost. This URI is needed for .NET Core App, so if you are using another platform suck as .NET Framework, you might need to check MSAL documentation for the redirect URI.
- Tick ID tokens to enabled id_token scope, which is used by the OIDC authentication.
- In tab Expose an API, click Add a scope, accept the default API URI and any scope name that you want (e.g. readwrite) and fill in required info.
- After the scope is added, copy the scope URI into the Scopes constants in the WPF app below.
Notes
- You don’t need to create any Client secrets for the authentication flows in this post as the authentication will be protected by Redirect URIs.
ASP.NET Code
A sample repository is prepared at https://github.com/nguyenquyhy/Delegate-AspNet-Authentication. You are encouraged to look at each commit as they demonstrate the steps of the implementation:
- An empty ASP.NET 5 project is created.
- Two pairs of MVC endpoints are added: one pair are MVC pages and the other pair are REST APIs. One endpoint in each pair does not require authentication while the other one requires authentication with the [Authorize] attribute.
- Two GraphQL endpoints are added in a similar authorization setup.
- A standard cookie & OIDC setup is configured, allowing automatic redirection to Microsoft Account login page. An authentication cookie will be created automatically after logged in, allowing accessing to authorized MVC and GraphQL endpoints from web browser.
- Add a WPF client to test authentication with JWT in Bearer Authorization header.
- Implement Sign In flow in the WPF client using MSAL library. At this moment, both calls to authorized REST and GraphQL APIs would not work because the web app is not configured to understand the JWT in Authorization header yet.
- Add configuration to make web app understand JWT and create appropriate ClaimsPrincipal. After this step, MVC endpoints (both views and APIs) can authenticate by both cookies and JWT.
- Add an Endpoint for /GraphQL to allow Authorization middleware to trigger default policy. After this step, GraphQL endpoint will also be able to authenticate by both cookies and JWT.
- Add endpoint to logout in web.
- Add button to logout in WPF client.
Issuer validation in step 3
If you setup your application in Azure AD to support any organization directory (multitenant) and Microsoft account, the returned issuers will contain the tenant ID and hence can be different from user to user (e.g. https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dae/v2.0). This means you cannot validate the issuer by simply preset a list of valid issuers.
In this case, a custom issuer validation logic is needed as seen in the code.
Some more explanation for step 8
In order to trigger both authentication schemes (i.e. JWT and Cookie), we need to either set AuthenticationSchemes property of each AuthorizeAttribute, or set it globally with a default policy (https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1#use-multiple-authentication-schemes). Both the AuthorizeAttribute and the default policy are activated by Authorization middleware, which in turns relies on Endpoint created by Routing middleware. This works well for MVC endpoints (e.g. actions, Razor pages).
However, routing middleware does not recognize GraphQL endpoint (i.e. /GraphQL), so it does not create any corresponding Endpoint instance. This means the Authorization middleware does not see any [Authorize] attribute (note that HotChocolate has different [Authorize] attribute with the same name) nor it will apply the default policy. Hence, only the DefaultAuthenticationScheme is picked up (i.e. Cookie) to create the ClaimsPrincipal.
To fix this issue, we manually create an Endpoint instance for GraphQL with the needed attribute.
Retaining user profile
After getting the identity information in terms of ClaimsPrincipal, your applicable will have to associate the identity claim with a user profile and store in your database. This is not discussed in this post as it depends on how you want to build your applications.
However, for Microsoft Account, you can look at 2 particular claims: sub or oid as the user ID for your system. You can read more about the two claims at https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens.
Reference
- https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2
- https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2
- https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2
- https://github.com/Azure-Samples/ms-identity-dotnet-desktop-tutorial
- https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki
- https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow
- https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens