-
Notifications
You must be signed in to change notification settings - Fork 0
Step by Step Instruction (ASP.NET core 2.0)
CodeGx edited this page Apr 2, 2018
·
5 revisions
- Install Microsoft.Identity.Client in your NuGet package manager: The package is currently in preview so you need to check Include prerelease to search for it. This is the package which makes it easier to obtain tokens from Microsoft Azure AD B2C.
-
Modify appsettings.json: add the following configuration details for Azure AD B2C.
"Authentication": {
"AzureAdB2C": {
"Tenant": "dnvglb2ctest.onmicrosoft.com",
"TenantId": "ed815121-cdfa-4097-b524-e2b23cd36eb6",
"ClientId": "**Your Client ID**",
"ClientSecret": "**Your Client Secret**",
"RedirectUri": "https://localhost:44330/signin-oidc",
"SignUpSignInPolicyId": "B2C_1A_SignInWithADFSIdp",
"ResetPasswordPolicyId": "B2C_1A_SignInWithADFSIdp",
"EditProfilePolicyId": "B2C_1A_SignInWithADFSIdp",
"ApiUrl": "https://myapiv3test.dnvgl.com",
"ApiScopes": "https://dnvglb2ctest.onmicrosoft.com/a4a8e726-c1cc-407c-83a0-4ce37f1ce130/user_impersonation"
}
},
"VeracityService": {
"HostName": "https://myapiv3test.dnvgl.com",
"Identifier": "https://dnvglb2ctest.onmicrosoft.com/a4a8e726-c1cc-407c-83a0-4ce37f1ce130",
"ServiceId": "**Your Service ID**"
}
- Create a class to map your settings:
public class AzureAdB2COptions
{
public const string PolicyAuthenticationProperty = "Policy";
public AzureAdB2COptions()
{
AzureAdB2CInstance = "https://login.microsoftonline.com";
}
public string ClientId { get; set; }
public string AzureAdB2CInstance { get; set; }
public string Tenant { get; set; }
public string TenantId { get; set; }
public string SignUpSignInPolicyId { get; set; }
public string SignInPolicyId { get; set; }
public string SignUpPolicyId { get; set; }
public string ResetPasswordPolicyId { get; set; }
public string EditProfilePolicyId { get; set; }
public string RedirectUri { get; set; }
public string DefaultPolicy => SignUpSignInPolicyId;
public string Authority => $"{AzureAdB2CInstance}/tfp/{Tenant}/{DefaultPolicy}/v2.0";
public string ClientSecret { get; set; }
public string ApiUrl { get; set; }
public string ApiScopes { get; set; }
public string MicrosoftGraphScope { get; set; }
public string MicrosoftGraphQuery { get; set; }
}
4.Edit Startup.cs: Add the following code. Ignore the error for now. In the ConfigureServices method
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
.AddCookie();
In the Configure method
app.UseSession();
app.UseAuthentication();
- Create a class to setup openid:
public static class AzureAdB2CAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder)
=> builder.AddAzureAdB2C(_ =>
{
});
public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action<AzureAdB2COptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();
builder.AddOpenIdConnect();
return builder;
}
public class OpenIdConnectOptionsSetup : IConfigureNamedOptions<OpenIdConnectOptions>
{
public OpenIdConnectOptionsSetup(IOptions<AzureAdB2COptions> b2cOptions)
{
AzureAdB2COptions = b2cOptions.Value;
}
public AzureAdB2COptions AzureAdB2COptions { get; set; }
public void Configure(string name, OpenIdConnectOptions options)
{
options.ClientId = AzureAdB2COptions.ClientId;
options.Authority = AzureAdB2COptions.Authority;
options.UseTokenLifetime = true;
options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "userId" };
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
OnRemoteFailure = OnRemoteFailure,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived
};
}
public void Configure(OpenIdConnectOptions options)
{
Configure(Options.DefaultName, options);
}
public Task OnRedirectToIdentityProvider(RedirectContext context)
{
var defaultPolicy = AzureAdB2COptions.DefaultPolicy;
if (context.Properties.Items.TryGetValue(AzureAdB2COptions.PolicyAuthenticationProperty, out var policy) &&
!policy.Equals(defaultPolicy))
{
context.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile;
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower().Replace(defaultPolicy.ToLower(), policy.ToLower());
context.Properties.Items.Remove(AzureAdB2COptions.PolicyAuthenticationProperty);
}
else if (!string.IsNullOrEmpty(AzureAdB2COptions.ApiUrl))
{
context.ProtocolMessage.Scope += $" offline_access {AzureAdB2COptions.ApiScopes}";
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.CodeIdToken;
}
return Task.FromResult(0);
}
public Task OnRemoteFailure(RemoteFailureContext context)
{
context.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
context.Response.Redirect("/Session/ResetPassword");
}
else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
{
context.Response.Redirect("/");
}
else
{
context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
//context.Response.Redirect("/Home/Error?message=" + "unauthorized");
}
return Task.FromResult(0);
}
public async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
// Use MSAL to swap the code for an access token
// Extract the code from the response notification
var code = context.ProtocolMessage.Code;
string signedInUserID = context.Principal.FindFirst("userId")?.Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, context.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, AzureAdB2COptions.ApiScopes.Split(' '));
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
}
}
- Create a class to manage token cache:
public class MSALSessionCache
{
private static ReaderWriterLockSlim SessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
string UserId = string.Empty;
string CacheId = string.Empty;
HttpContext httpContext = null;
TokenCache cache = new TokenCache();
public MSALSessionCache(string userId, HttpContext httpcontext)
{
// not object, we want the SUB
UserId = userId;
CacheId = UserId + "_TokenCache";
httpContext = httpcontext;
Load();
}
public TokenCache GetMsalCacheInstance()
{
cache.SetBeforeAccess(BeforeAccessNotification);
cache.SetAfterAccess(AfterAccessNotification);
Load();
return cache;
}
public void SaveUserStateValue(string state)
{
SessionLock.EnterWriteLock();
httpContext.Session.SetString(CacheId + "_state", state);
SessionLock.ExitWriteLock();
}
public string ReadUserStateValue()
{
string state = string.Empty;
SessionLock.EnterReadLock();
state = (string)httpContext.Session.GetString(CacheId + "_state");
SessionLock.ExitReadLock();
return state;
}
public void Load()
{
SessionLock.EnterReadLock();
cache.Deserialize(httpContext.Session.Get(CacheId));
SessionLock.ExitReadLock();
}
public void Persist()
{
SessionLock.EnterWriteLock();
// Optimistically set HasStateChanged to false. We need to do it early to avoid losing changes made by a concurrent thread.
cache.HasStateChanged = false;
// Reflect changes in the persistent store
httpContext.Session.Set(CacheId, cache.Serialize());
SessionLock.ExitWriteLock();
}
// Triggered right before MSAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Load();
}
// Triggered right after MSAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (cache.HasStateChanged)
{
Persist();
}
}
}
- Create a controller to handle authentication schemes:
public class SessionController : Controller
{
public SessionController(IOptions<AzureAdB2COptions> b2cOptions)
{
AzureAdB2COptions = b2cOptions.Value;
}
public AzureAdB2COptions AzureAdB2COptions { get; set; }
[HttpGet]
public IActionResult SignIn()
{
var redirectUrl = Url.Action(nameof(HomeController.Index), "Home");
return Challenge(
new AuthenticationProperties { RedirectUri = redirectUrl },
OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult ResetPassword()
{
var redirectUrl = Url.Action(nameof(HomeController.Index), "Home");
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
properties.Items[AzureAdB2COptions.PolicyAuthenticationProperty] = AzureAdB2COptions.ResetPasswordPolicyId;
return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult EditProfile()
{
var redirectUrl = Url.Action(nameof(HomeController.Index), "Home");
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
properties.Items[AzureAdB2COptions.PolicyAuthenticationProperty] = AzureAdB2COptions.EditProfilePolicyId;
return Challenge(properties, OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult SignOut()
{
var callbackUrl = Url.Action(nameof(SignedOut), "Session", values: null, protocol: Request.Scheme);
return SignOut(new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpGet]
public IActionResult SignedOut()
{
if (User.Identity.IsAuthenticated)
{
// Redirect to home page if the user is authenticated.
return RedirectToAction(nameof(HomeController.Index), "Home");
}
return View();
}
}
- Protect your web action with Authorize attribute:
[Authorize]
public async System.Threading.Tasks.Task<IActionResult> Index()
{
var name = User.DnvglAccountName();
if (name != null)
{
ViewData["UserId"] = name;
var mydnvglid = User.MyDnvglId(); //Please refer to the Veracity Claims page
var serviceSubscription = await GetServiceSubscription(_appSettings.VeracityService.ServiceId, mydnvglid);
if (serviceSubscription.StartsWith("http"))
{
return Redirect(serviceSubscription);
}
else
{
return View();
}
}
else
{
return RedirectToAction("Error");
}
}
The latest version of Veracity API is 3.0. Link: https://myapiv3test.dnvgl.com/