My ASP.NET MVC Core application uses OWIN Middleware along with the following modules to perform OpenIdConnect authentication against Azure AD:
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Azure.ActiveDirectory.GraphClient;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Azure.ActiveDirectory.GraphClient.Extensions;
The OWIN Middleware performs a bunch of tasks including
- Fetching Azure AD Groups and Roles via Azure Graph API
- Fetching User Profile Data from Database
- Creating Claims from steps 1 & 2
- Issuing cookie
- The Middleware automatically handles Refresh tokens
- The Middleware caches the token in the database and able to retrieve via a mechanism
AcquireTokenSilentAsync
for Graph client.
The MVC application serves a single Razor view and from that point onward, I am using Aurelia JavasScript framework (could easily be Angular, Knockout, React, not important) which only performs API requests to my Api Controller via AJAX.
So my question is how to convert all these authentication and authorization steps handled on the server to JWT based authentication on the client against Azure AD?
Admittedly, my question is fairly naive as there is substantial work being performed by OWIN Middleware components in the code below. So I am looking for a starting point, helper libraries and feasibility. I don't feel confident removing all the middleware code and server side authentication until I am confident this flow can be replicated using AJAX and JWT authentication.
I have done some research and the answer may involve the following
- adal.js
- JWT middleware in ASP.NET Core
- HTML Web Storage
- Azure AD Graph REST API (instead of C# Graph Client)
Here is the current OWIN Middleware code performing OpenIdConnect authentication against Azure AD on the server:
app.UseCookieAuthentication();
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
ClientSecret = Configuration["Authentication:AzureAd:ClientSecret"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
Events = new OpenIdConnectEvents()
{
OnAuthorizationCodeReceived = async (context) =>
{
var code = context.TokenEndpointRequest.Code;
var identity = context.Ticket.Principal.Identity as ClaimsIdentity;
userObjectID = identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
signedInUserID = identity.FindFirst(ClaimTypes.NameIdentifier).Value;
ClientCredential credential =
new ClientCredential(
Configuration["Authentication:AzureAd:ClientId"],
Configuration["Authentication:AzureAd:ClientSecret"]);
var authority = Configuration["Authentication:AzureAd:AADInstance"]
+ Configuration["Authentication:AzureAd:TenantId"];
AuthenticationContext authContext =
new AuthenticationContext(authority, new ADALTokenCacheService(signedInUserID, Configuration));
await authContext.AcquireTokenByAuthorizationCodeAsync(
context.TokenEndpointRequest.Code,
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute),
credential,
Configuration["Authentication:AzureAd:GraphResource"]);
context.HandleCodeRedemption();
ActiveDirectoryClient activeDirectoryClient = GetActiveDirectoryClient();
// Get currently logged in User from Graph
IPagedCollection<IUser> users = await activeDirectoryClient.Users.Where(u => u.ObjectId.Equals(userObjectID)).ExecuteAsync();
IUser user = users.CurrentPage.ToList().First();
// Get User's AD Groups
IEnumerable<string> userGroupIds = await user.GetMemberGroupsAsync(false);
List<string> userGroupIdList = userGroupIds.ToList();
// Transform User's AD Groups into Claims
foreach (var groupObjectId in userGroupIdList)
{
var group = await activeDirectoryClient.Groups.GetByObjectId(groupObjectId).ExecuteAsync();
Claim newClaim = new Claim(
CustomClaimValueTypes.ADGroup,
group.DisplayName,
ClaimValueTypes.String,
"AAD GRAPH");
((ClaimsIdentity)(context.Ticket.Principal.Identity)).AddClaim(newClaim);
}
// Get User's Application permissions from Database
upn = identity.FindFirst(ClaimTypes.Upn).Value;
DbContext db =
new DbContext(Configuration["ConnectionStrings:DefaultConnection"]);
if (db.PortalUsers.FirstOrDefault(b => (b.UPN == upn)) == null)
{
throw new System.IdentityModel.Tokens.SecurityTokenValidationException("You are not registered to use this application.");
}
var applications = from permissions in db.PortalPermissions
where permissions.PortalUser.UPN == upn
//orderby permissions.Application.SortOrder ascending
select permissions.PortalApplication;
// Transform User's Application permissions into Claims
foreach (var application in applications)
{
Claim newClaim = new Claim(
CustomClaimValueTypes.Application,
application.Name,
ClaimValueTypes.String,
"DATABASE");
((ClaimsIdentity)(context.Ticket.Principal.Identity)).AddClaim(newClaim);
}
},
OnRemoteFailure = (context) =>
{
if (context.Failure.Message == "You are not registered to use this application.")
{
context.Response.Redirect("/AuthenticationError");
}
else
{
context.Response.Redirect("/Error");
}
context.HandleResponse();
return Task.FromResult(0);
}
}
});
app.UseFileServer(new FileServerOptions
{
EnableDefaultFiles = true,
EnableDirectoryBrowsing = false
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Start}/{id?}");
});
}
private ActiveDirectoryClient GetActiveDirectoryClient()
{
Uri servicePointUri = new Uri(Configuration["Authentication:AzureAd:GraphResource"]);
Uri serviceRoot = new Uri(servicePointUri, Configuration["Authentication:AzureAd:TenantId"]);
ActiveDirectoryClient activeDirectoryClient = new ActiveDirectoryClient(
serviceRoot, async () => await GetTokenForApplicationAsync());
return activeDirectoryClient;
}
private async Task<string> GetTokenForApplicationAsync()
{
ClientCredential clientCredential =
new ClientCredential(
Configuration["Authentication:AzureAd:ClientId"],
Configuration["Authentication:AzureAd:ClientSecret"]);
AuthenticationContext authenticationContext =
new AuthenticationContext(
Configuration["Authentication:AzureAd:AADInstance"] +
Configuration["Authentication:AzureAd:TenantId"],
new ADALTokenCacheService(signedInUserID, Configuration));
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(
Configuration["Authentication:AzureAd:GraphResource"],
clientCredential,
new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
return authenticationResult.AccessToken;
}