1

Im using Azure ADB2C for authentication and authorization and using Open Id Connect flow. I need to do redirect the user to the application home page with retrieved access token attached to the home page url as query parameter from "OnAuthorizationCodeReceived". But that doesn't work. Below is my code.

By doing this I intend to catch access token and id token from redirect URL. And this is an single page application . I tried MSAL.js but it has issues in browser compatibility and even some times in different identity providers. So I decided to user open id from c# backend

  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.CallbackPath = new Microsoft.AspNetCore.Http.PathString("/console/home");
            options.TokenValidationParameters = new TokenValidationParameters() { SaveSigninToken=true, NameClaimType = "name" };
            options.SaveTokens = true;
            options.Events = new OpenIdConnectEvents()
            {
                OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
                OnRemoteFailure = OnRemoteFailure,
                OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
                OnTokenValidated= OnTokenValidated,
                OnTokenResponseReceived= OnTokenResponseReceived
            };
        }

        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);
            }
            return Task.FromResult(0);
        }

        public 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(ClaimTypes.NameIdentifier).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
            {
                List<string> apiScopes = new List<string>();

                AuthenticationResult result =  cca.AcquireTokenByAuthorizationCodeAsync(code, AzureAdB2COptions.ApiScopes.Split(' ')).Result;

                //context.HandleResponse();
                context.HandleCodeRedemption(result.AccessToken, result.IdToken);
                context.Response.Redirect("http://localhost:8836/console/home?id_token="+result.IdToken+"&access_token="+result.AccessToken);
                return Task.FromResult(0);
                //context.HandleCodeRedemption(result.AccessToken, result.IdToken);

            }
            catch (Exception ex)
            {
                //TODO: Handle
                throw;
            }
        }

        public Task OnTokenValidated(TokenValidatedContext context)
        {
            try
            {
                return Task.FromResult(0);
            }
            catch (Exception ex)
            {

                throw;
            }
        }

        public Task OnTokenResponseReceived(TokenResponseReceivedContext context)
        {
            try
            {
                var cntxt = context;
                context.ProtocolMessage.RedirectUri = "/console/home";
                context.Response.Redirect("/Home/Error?message=test");
                return Task.FromResult(0);
            }
            catch (Exception ex)
            {

                throw;
            }
        }
    }
}

And this is my startup class

 public void ConfigureServices(IServiceCollection services)
    {

        services.Configure<AzureAdB2COptions>(Configuration.GetSection("Authentication:AzureAdB2C"));
        services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();

        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
        .AddCookie();

        services.AddCors(options =>
        {
            options.AddPolicy("CorsPolicy",
                builder => builder.AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
        });

        services.AddMvc();

        // Adds a default in-memory implementation of IDistributedCache.
        services.AddDistributedMemoryCache();
        services.AddSession(options =>
        {
            options.IdleTimeout = TimeSpan.FromHours(1);
            options.CookieHttpOnly = true;
        });
    }
Suful
  • 39
  • 1
  • 6
  • Add the code where you subscribe this event. – Max Dec 24 '18 at 10:11
  • @Max hi max, this OnAuthorizationCodeReceieved triggers when i enter my credentials at login.microsoftonline.com B2C site. – Suful Dec 24 '18 at 10:48
  • @Max Hi max I updated the code. Please have a look :) – Suful Dec 24 '18 at 10:54
  • This look like a IConfigureNamedOptions. 1. Can you post the entire class ? 2. Can you post also the code where you bind this class ? 3. Have you tried a breakpoint to see if enter the code ? 4. What mean "It doesn't work ? – Max Dec 24 '18 at 11:07
  • @Max Updated the code. Yes Max tried with debugging. Doesn't work means that it doesn't redirect user to the page where i have asked to redirect("console/home"+token) – Suful Dec 24 '18 at 11:25
  • You have a front-end framework for your SPA (maybe with a router ) ? You try to redirect to this front-end url ? See this [stackoverflow similar question](https://stackoverflow.com/questions/32488171/redirect-user-after-authentication-with-openidconnect-in-asp-net-mvc) – Max Dec 24 '18 at 12:15
  • What is context.HandleResponse();. why it is used for? – Suful Dec 24 '18 at 13:05
  • @Max what i lacked in my code was context.HandleResponse() method to be called. Now it works thanks for the help. If you can put your above comment as answer i will mark it as the answer – Suful Dec 25 '18 at 01:24

1 Answers1

3

You have to call

context.HandleResponse();

This method handle the response from the Auth server and grab the JWT.

On Microsoft Doc you can find additional info about OpenId Connect.

Max
  • 6,821
  • 3
  • 43
  • 59