2

I have been tasked with adding an mvc website to an existing web api project that uses Identity Server 4. Currently the web api authenticates Android clients using bearer tokens.

I would like the mvc site to use cookies which contain the bearer token.

I have been struggling with this for a few days now and I can make either the web client work, or the android client work, but not both. If I remove the default scheme, from the AddAuthentication(), the website will work as intended. This however causes the android client to not authorize users (HttpClient.User.Claims is null). If I do add the default scheme, the android client will work, but the website will not pick up the claims.

Any ideas on how to get this to work?

Image showing no claims being picked up

Here is my current setup:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) //TODO If I remove this default then the android client stops working (the web client will only work with this removed)
           .AddCookie(options =>
           {
               options.LoginPath = new PathString("/Development/Account/Login");
               options.AccessDeniedPath = new PathString("/Development/Account/Login");
               options.LogoutPath = new PathString("/Development/Account/Logout");
               options.ExpireTimeSpan = TimeSpan.FromDays(1);
           })
           .AddIdentityServerAuthentication(options => 
           {
               var configAuthority = container.GetInstance<IOptions<CoPilotConfig>>().Value?.IdentityAuthority;
               options.Authority = configAuthority ?? $"http://localhost:{Constants.LOOPBACK_PORT}";
               options.RequireHttpsMetadata = false;
               options.ApiName = "CoPilotApi";
           }); 

Here is the method used to login on the website portion:

 [HttpPost("Login")]
    [AllowAnonymous]
    public async Task<ActionResult> Login(LoginModel model)
    {
        if (!ModelState.IsValid)
            return View(model);

        string deviceId = Guid.NewGuid().ToString("N");
        string userString = deviceId + @"\" + _ctx.Company + @"\" + model.Username;
        var accessToken = await GenerateTokenAsync(userString, model.Password);

        if (accessToken != "invalid_grant")
        {
            JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = handler.ReadToken(accessToken) as JwtSecurityToken;
            jwtToken.Claims.ToList();
            var claimsIdentity = new ClaimsIdentity(jwtToken.Claims.ToList(), IdentityServerConstants.DefaultCookieAuthenticationScheme);
            var authProperties = new AuthenticationProperties
            {
                AllowRefresh = true,
                ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1),
                IssuedUtc = DateTime.UtcNow
            };

            try
            {
                await HttpContext.SignInAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
            }
            catch (Exception e){}

            return RedirectToAction("Index", "Map");
        }
        else
        {
            model = new LoginModel();
            model.Exception = "Invalid username or password";
            return View("~/Views/Account/Login.cshtml", model);
        }
    }

    private async Task<string> GenerateTokenAsync(string username, string password)
    {
        var clientList = IdentityServerConfig.GetClients();
        Client client = clientList.FirstOrDefault();

        string tokenUrl = $"http://" + HttpContext.Request.Host.ToString() + "/connect/token";
        TokenClient tokenClient = new TokenClient(tokenUrl, client.ClientId, "secret");
        var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync(username, password, client.AllowedScopes.FirstOrDefault());
        if (tokenResponse.IsError)
            return "invalid_grant";

        return tokenResponse.AccessToken;
    }

Any ideas on how to fix this or another way of handling this issue without having to modify the Android client?

rhfrench
  • 21
  • 3

1 Answers1

0

I figured it out. To anyone trying this you add a middleware to determine which scheme to use for the incoming request:

 public class AuthSchemeMiddleware : IMiddleware
{
    public async Task InvokeAsync(HttpContext context, Func<Task> next)
    {
        var scheme = context.Request.Cookies.ContainsKey("idsrv.session")
            ? "Cookies" : "Bearer";

        var result = await context.AuthenticateAsync(scheme);
        if (result.Succeeded)
        {
            context.User = result.Principal;
        }
        await next();
    }
}

You will call the new middleware here:

 // use Authentication from IdentityServer4
 app.UseAuthentication();

 // Serve static files
 app.UseDefaultFiles();
 app.UseStaticFiles(new StaticFileOptions()
 {
     ContentTypeProvider = ContentTypeProviderFactory.GetContentTypeProvider()
 });

 app.UseMiddlewareFrom<AuthSchemeMiddleware>(container);
 app.UseMvcWithDefaultRoute();

Authentication should be added like this:

services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = "Cookies";
            })
           .AddCookie("Cookies", options =>
           {
               options.LoginPath = new PathString("/Development/Account/Login");
               options.AccessDeniedPath = new PathString("/Development/Account/Login");
               options.LogoutPath = new PathString("/Development/Account/Logout");
               options.ExpireTimeSpan = TimeSpan.FromDays(1);
           });

        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddIdentityServerAuthentication(options =>
            {
                var configAuthority = container.GetInstance<IOptions<CoPilotConfig>>().Value?.IdentityAuthority;
                options.Authority = configAuthority ?? $"http://localhost:{Constants.LOOPBACK_PORT}";
                options.RequireHttpsMetadata = false;
                options.ApiName = "CoPilotApi";
            });
rhfrench
  • 21
  • 3