Bonjour à vous,

Aujourd’hui je vous propose un article sur la mise en place d’une double authentification au sein d’une application ASP.NET Core 2.

Introduction

En effet, j’ai remarqué qu’en activant l’authentification Azure AD uniquement cela fonctionne parfaitement; par contre dès qu’on essaye d’activer l’authentification core identity pour avoir une authentification applicative ou azure AD une erreur survient sur la méthode GetExternalLoginInfoAsync: “Unable to get information from external login”.

Je vous propose donc de vous donner une mise en oeuvre possible qui fonctionne et qui consiste à effectuer l’implémentation d’extension AzureAD nous même.

Mise en place

Pour commencer, créez un dossier AzureAd au sein de votre projet. Ce dossier va contenir les 4 classes pour l’implémentation de notre extension.

Implémentation de l’extension

Créez ensuite au sein de ce dossier une classe AzureAdDefaults avec le contenu ci dessous:

public static class AzureAdDefaults
{
/// <summary> Name of the display. </summary>
public static readonly string DisplayName = "AzureAD";
/// <summary> The authorization endpoint. </summary>
public static readonly string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/authorize";
/// <summary> The token endpoint. </summary>
public static readonly string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/token";
/// <summary> The resource. </summary>
public static readonly string Resource = "https://graph.microsoft.com";
/// <summary> The user information endpoint. </summary>
public static readonly string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me";
/// <summary> The authentication scheme. </summary>
public const string AuthenticationScheme = "AzureAD";
}

Cette classe contient les valeurs par défaut nécessaires pour le fonctionnement de AzureAd.

Créez ensuite une classe AzureAdHandler avec le contenu suivant:

internal class AzureAdHandler : OAuthHandler<AzureAdOptions>
{
/// <summary> Constructor. </summary>
///
/// <param name="options"> Options for controlling the operation. </param>
/// <param name="logger"> The logger. </param>
/// <param name="encoder"> The encoder. </param>
/// <param name="clock"> The clock. </param>

public AzureAdHandler(IOptionsMonitor<AzureAdOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}

/// <summary> Creates ticket asynchronous. </summary>
///
/// <exception cref="HttpRequestException"> Thrown when a HTTP Request error condition occurs. </exception>
///
/// <param name="identity"> The identity. </param>
/// <param name="properties"> The properties. </param>
/// <param name="tokens"> The tokens. </param>
///
/// <returns> An asynchronous result that yields the create ticket. </returns>

protected override async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
{
HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
HttpResponseMessage httpResponseMessage = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (!httpResponseMessage.IsSuccessStatusCode)
throw new HttpRequestException(message: $"Failed to retrieve Azure AD user information ({httpResponseMessage.StatusCode}) Please check if the authentication information is correct and the corresponding Microsoft Account API is enabled.");
JObject user = JObject.Parse(await httpResponseMessage.Content.ReadAsStringAsync());
OAuthCreatingTicketContext context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, user);
context.RunClaimActions();
await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}

/// <summary> Exchange code asynchronous. </summary>
///
/// <param name="code"> The code. </param>
/// <param name="redirectUri"> URI of the redirect. </param>
///
/// <returns> An asynchronous result that yields the exchange code. </returns>

protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(string code, string redirectUri)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("grant_type", "authorization_code");
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("client_secret", Options.ClientSecret);
dictionary.Add(nameof(code), code);
dictionary.Add("resource", Options.Resource);

HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequestMessage.Content = new FormUrlEncodedContent(dictionary);
HttpResponseMessage response = await Backchannel.SendAsync(httpRequestMessage, Context.RequestAborted);
if (response.IsSuccessStatusCode)
return OAuthTokenResponse.Success(JObject.Parse(await response.Content.ReadAsStringAsync()));
return OAuthTokenResponse.Failed(new Exception(string.Concat("OAuth token endpoint failure: ", await Display(response))));
}

/// <summary> Builds challenge URL. </summary>
///
/// <param name="properties"> The properties. </param>
/// <param name="redirectUri"> URI of the redirect. </param>
///
/// <returns> A string. </returns>

protected override string BuildChallengeUrl(AuthenticationProperties properties, string redirectUri)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("client_id", Options.ClientId);
dictionary.Add("scope", FormatScope());
dictionary.Add("response_type", "code");
dictionary.Add("redirect_uri", redirectUri);
dictionary.Add("state", Options.StateDataFormat.Protect(properties));
dictionary.Add("resource", Options.Resource);
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, dictionary);
}

/// <summary> Displays the given response. </summary>
///
/// <param name="response"> The response. </param>
///
/// <returns> An asynchronous result that yields a string. </returns>

private static async Task<string> Display(HttpResponseMessage response)
{
StringBuilder output = new StringBuilder();
output.Append($"Status: { response.StatusCode };");
output.Append($"Headers: { response.Headers };");
output.Append($"Body: { await response.Content.ReadAsStringAsync() };");
return output.ToString();
}
}

Créez une classe AzureAdOptions avec le contenu suivant:

public class AzureAdOptions : OAuthOptions
{
/// <summary> Gets or sets the instance. </summary>
///
/// <value> The instance. </value>

public string Instance { get; set; }

/// <summary> Gets or sets the resource. </summary>
///
/// <value> The resource. </value>

public string Resource { get; set; }

/// <summary> Gets or sets the identifier of the tenant. </summary>
///
/// <value> The identifier of the tenant. </value>

public string TenantId { get; set; }

/// <summary> Default constructor. </summary>

public AzureAdOptions()
{
CallbackPath = new PathString("/signin-azureAd");
AuthorizationEndpoint = AzureAdDefaults.AuthorizationEndpoint;
TokenEndpoint = AzureAdDefaults.TokenEndpoint;
UserInformationEndpoint = AzureAdDefaults.UserInformationEndpoint;
Resource = AzureAdDefaults.Resource;
Scope.Add("user.read");

ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName");
ClaimActions.MapJsonKey(ClaimTypes.GivenName, "givenName");
ClaimActions.MapJsonKey(ClaimTypes.Surname, "surname");
ClaimActions.MapJsonKey(ClaimTypes.MobilePhone, "mobilePhone");
ClaimActions.MapCustomJson(ClaimTypes.Email, user => user.Value<string>("mail") ?? user.Value<string>("userPrincipalName"));
}
}

Il nous reste maintenant à créer une classe extension pour nos AddAzureAD.

Créez une classe AzureAdExtensions avec le contenu suivant:

public static class AzureAdExtensions
{
/// <summary>
/// An AuthenticationBuilder extension method that adds an azure ad to 'configureOptions'.
/// </summary>
///
/// <param name="builder"> The builder to act on. </param>
///
/// <returns> An AuthenticationBuilder. </returns>

public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
=> builder.AddAzureAd(_ => { });

/// <summary>
/// An AuthenticationBuilder extension method that adds an azure ad to 'configureOptions'.
/// </summary>
///
/// <param name="builder"> The builder to act on. </param>
/// <param name="configureOptions"> Options for controlling the configure. </param>
///
/// <returns> An AuthenticationBuilder. </returns>

public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
return builder.AddOAuth<AzureAdOptions, AzureAdHandler>(AzureAdDefaults.AuthenticationScheme, AzureAdDefaults.DisplayName, configureOptions);
}

/// <summary>
/// A Microsoft.AspNetCore.Mvc.RazorPages.PageModel extension method that challenge azure a
/// d.
/// </summary>
///
/// <param name="pageModel"> The page model. </param>
/// <param name="signInManager"> Manager for sign in. </param>
/// <param name="redirectUrl"> URL of the redirect. </param>
///
/// <returns> A ChallengeResult. </returns>

public static ChallengeResult ChallengeAzureAD(this Microsoft.AspNetCore.Mvc.RazorPages.PageModel pageModel, SignInManager<DelssiSkillsCenterUser> signInManager, string redirectUrl)
//public static ChallengeResult ChallengeAzureAD(this ControllerBase controllerBase, SignInManager<ApplicationUser> signInManager, string redirectUrl)
{
return pageModel.Challenge(signInManager.ConfigureExternalAuthenticationProperties(AzureAdDefaults.AuthenticationScheme, redirectUrl), AzureAdDefaults.AuthenticationScheme);
}
}

Nos classes sont prêtes, passons à la configuration de notre application azureAd

Création de l’application AzureAd et configuration

Connectez vous sur votre portail azure et accéder à votre AZURE AD.

Accédez à App Registration:

Cliquez sur “Create” afin de créer une application et saisir les champs ci dessous:

Une fois votre application créée vous avez besoin de configurer les permissions de votre application. Je ne détail pas cette partie mais si vous voulez un article sur cette partie n’hésitez pas à le faire savoir en commentaire.

Vous avez besoin ensuite de récupérer l’application ID et tenantID sur la page de votre application.

N’oubliez pas de configurer votre reply url au sein de votre application qui est l’url qui sera en redirection après l’authentification effectuée.

N’oubliez pas également de générer un clientSecret qui sera utilisé par votre application, vous pourrez effectuer cette action en vous rendant sur Certificates & Secret puis new client secret.

Configuration de notre application ASP.NET Core 2

Pour finir, il ne reste plus qu’a configurer notre application ASP.NET Core afin de spécifier nos différents paramètres tel que le clientId, TenantId, callbackurl…

Vous trouverez ci dessous la configuration à ajouter dans votre appsettings.json production et développement. Je vous conseil de créer une application AzureAd distinct pour la production et pour le développement.

"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "Votre Domaine",
"TenantId": "Votre tenant ID",
"ClientId": "Votre callback url",
"CallbackPath": "Votre reply url sans le domaine",
"ClientSecret": "Votre client secret"
}

Il faut ensuite charger  les middleware et configurer les services comme ceci:

public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddAzureAd(options =>
{
options.ClientId = Configuration["AzureAd:ClientId"];
options.AuthorizationEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/authorize";
options.TokenEndpoint = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}/oauth2/token";
options.ClientSecret = Configuration["AzureAd:ClientSecret"];
options.CallbackPath = Configuration["AzureAd:CallbackPath"];
});

}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, SeedData seeder)
{

app.UseAuthentication();

}

Une fois cette configuration faites, les utilisateurs pourront soit s’authentifier via un compte applicatif soit pas l’Azure AD. A noter qu’a la première connexion via Azure AD un “profil” est créé dans la base applicative mais bien évidemment sans mot de passe.

Si toutefois vous souhaitez personnaliser les différents écrans d’authentification vous pouvez scaffolder l’identity via visual studio en faisant un clique droit sur votre projet > Ajouter > Ajouter un élément généré automatiquement.

Puis sélectionnez Identity dans identité:

Sur l’écran suivant sélectionnez les vues que vous souhaitez puis remplissez les différentes informations notamment votre layout si vous avez votre propre layout, le contexte de donnée et la classe utilisateur.

Vous obtiendrez ainsi la structure suivante qui vous permettra de customiser l’authentification et également faire le tri avec des fonctionnalités dont vous ne voulez pas.

Pour finir vous remarquez une classe IdentityHostingStartup. Cette classe contient la configuration pour l’identité notamment la configuration du DB Context ainsi que le AddIdentity n’oubliez donc pas de la complétée.

public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) => {
services.AddDbContext<MyIdentityContext>(options =>
options.UseSqlServer(
context.Configuration.GetConnectionString("MyContextConnection")));

services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<MyIdentityContext>().AddDefaultUI().AddDefaultTokenProviders();

});
}

J’espère que ce post vous aura éclairé sur la manière d’effectuer l’implémentation de la double authentification au sein de votre application, si toutefois vous avez des questions n’hésitez pas à les poser en commentaire.

Développez bien et à la prochaine 😉