11

I'm trying to migrate this project https://github.com/asadsahi/AspNetCoreSpa from .net core 1.1 to 2.0 but have a problem after a successful login. After the login my GET api calls e. g. to https://localhost:44331/api/profile/test end up with a redirect (302) and I don't know why. I received a bearer token and it looks fine.

Request header format: authorization: Bearer [token]

[Route("api/[controller]")]
public class ProfileController : BaseController
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger _logger;

    public ProfileController(ILoggerFactory loggerFactory, UserManager<ApplicationUser> userManager)
    {
        _logger = loggerFactory.CreateLogger<ProfileController>();
        _userManager = userManager;
    }

    [HttpGet("test")]
    public async Task<IActionResult> Test()
    {
        return  Json(ModelState.GetModelErrors());
    }
}

[Authorize]
[ServiceFilter(typeof(ApiExceptionFilter))]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public class BaseController : Controller
{
    public BaseController()
    {
    }
}

Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    if (_hostingEnv.IsDevelopment())
    {
        services.AddSslCertificate(_hostingEnv);
    }
    else
    {
        services.Configure<MvcOptions>(o => o.Filters.Add(new RequireHttpsAttribute()));
    }
    services.AddOptions();
    services.AddCors();
    services.AddLogging();
    services.AddResponseCompression(options =>
    {
        options.MimeTypes = Helpers.DefaultMimeTypes;
    });

    services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;


    }).AddJwtBearer(cfg =>
    {
        cfg.SaveToken = true;
        cfg.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = Configuration["Authentication:BearerTokens:Issuer"],
            ValidAudience = Configuration["Authentication:BearerTokens:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Authentication:BearerTokens:Key"])),
            ValidateIssuerSigningKey = false,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
        cfg.Events = new JwtBearerEvents
        {

            OnAuthenticationFailed = context =>
            {
                var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                logger.LogError("Authentication failed.", context.Exception);
                return Task.CompletedTask;
            },

            OnMessageReceived = context =>
            {
                return Task.CompletedTask;
            },
            OnChallenge = context =>
            {
                var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(JwtBearerEvents));
                logger.LogError("OnChallenge error", context.Error, context.ErrorDescription);
                return Task.CompletedTask;
            }
        };
    });

    services.AddDbContext<ApplicationDbContext>(options =>
    {
        string useSqLite = Startup.Configuration["Data:useSqLite"];
        if (useSqLite.ToLower() == "true")
        {
            options.UseSqlite(Startup.Configuration["Data:SqlLiteConnectionString"]);
        }
        else
        {
            options.UseSqlServer(Startup.Configuration["Data:SqlServerConnectionString"]);
        }
        options.UseOpenIddict();
    });


    services.AddIdentity<ApplicationUser, ApplicationRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    //services.ConfigureApplicationCookie(options =>
    //{

    //    options.LoginPath = "/login";
    //    options.Events.OnRedirectToLogin = context =>
    //    {
    //        if (context.Request.Path.StartsWithSegments("/api") &&
    //            context.Response.StatusCode == (int)HttpStatusCode.OK)
    //        {
    //            context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
    //        }
    //        else
    //        {
    //            context.Response.Redirect(context.RedirectUri);
    //        }
    //        return Task.FromResult(0);
    //    };
    //});


    services.AddOAuthProviders();

    services.AddCustomOpenIddict();

    services.AddMemoryCache();

    services.RegisterCustomServices();

    services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

    services.AddCustomizedMvc();

    // Node services are to execute any arbitrary nodejs code from .net
    services.AddNodeServices();

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "AspNetCoreSpa", Version = "v1" });
    });
}

public void Configure(IApplicationBuilder app)
{
    app.AddDevMiddlewares();

    if (_hostingEnv.IsProduction())
    {
        app.UseResponseCompression();
    }

    app.SetupMigrations();

    app.UseXsrf();

    app.UseStaticFiles();

    app.UseAuthentication();

    app.UseMvc(routes =>
    {
        // http://stackoverflow.com/questions/25982095/using-googleoauth2authenticationoptions-got-a-redirect-uri-mismatch-error
        routes.MapRoute(name: "signin-google", template: "signin-google", defaults: new { controller = "Account", action = "ExternalLoginCallback" });

        routes.MapSpaFallbackRoute(
            name: "spa-fallback",
            defaults: new { controller = "Home", action = "Index" });
    });
}

My IServiceCollection-Extensions:

public static IServiceCollection AddCustomizedMvc(this IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Filters.Add(typeof(ModelValidationFilter));
    })
    .AddJsonOptions(options =>
    {
        options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    });

    return services;
}

public static IServiceCollection AddOAuthProviders(this IServiceCollection services)
{
    services.AddAuthentication()
        .AddFacebook(o =>
        {
            o.AppId = Startup.Configuration["Authentication:Facebook:AppId"];
            o.AppSecret = Startup.Configuration["Authentication:Facebook:AppSecret"];
        });

    services.AddAuthentication()
        .AddGoogle(o =>
        {
            o.ClientId = Startup.Configuration["Authentication:Google:ClientId"];
            o.ClientSecret = Startup.Configuration["Authentication:Google:ClientSecret"];
        });
    services.AddAuthentication()
        .AddTwitter(o =>
        {
            o.ConsumerKey = Startup.Configuration["Authentication:Twitter:ConsumerKey"];
            o.ConsumerSecret = Startup.Configuration["Authentication:Twitter:ConsumerSecret"];
        });

    services.AddAuthentication()
        .AddMicrosoftAccount(o =>
        {
            o.ClientId= Startup.Configuration["Authentication:Microsoft:ClientId"];
            o.ClientSecret = Startup.Configuration["Authentication:Microsoft:ClientSecret"];
        });

    return services;
}

public static IServiceCollection AddCustomOpenIddict(this IServiceCollection services)
{

    // Configure Identity to use the same JWT claims as OpenIddict instead
    // of the legacy WS-Federation claims it uses by default (ClaimTypes),
    // which saves you from doing the mapping in your authorization controller.
    services.Configure<IdentityOptions>(options =>
    {
        options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
        options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
        options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;

    });

    // Register the OpenIddict services.
    services.AddOpenIddict()
        // Register the Entity Framework stores.
        .AddEntityFrameworkCoreStores<ApplicationDbContext>()

        // Register the ASP.NET Core MVC binder used by OpenIddict.
        // Note: if you don't call this method, you won't be able to
        // bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
        .AddMvcBinders()

        // Enable the token endpoint.
        .EnableTokenEndpoint("/connect/token")

        // Enable the password and the refresh token flows.
        .AllowPasswordFlow()
        .AllowRefreshTokenFlow()

        // During development, you can disable the HTTPS requirement.
        .DisableHttpsRequirement()

        // Register a new ephemeral key, that is discarded when the application
        // shuts down. Tokens signed using this key are automatically invalidated.
        // This method should only be used during development.
        .AddEphemeralSigningKey();

    // On production, using a X.509 certificate stored in the machine store is recommended.
    // You can generate a self-signed certificate using Pluralsight's self-cert utility:
    // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip
    //
    // services.AddOpenIddict()
    //     .AddSigningCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75");
    //
    // Alternatively, you can also store the certificate as an embedded .pfx resource
    // directly in this assembly or in a file published alongside this project:
    //
    // services.AddOpenIddict()
    //     .AddSigningCertificate(
    //          assembly: typeof(Startup).GetTypeInfo().Assembly,
    //          resource: "AuthorizationServer.Certificate.pfx",
    //          password: "OpenIddict");

    return services;
}

public static IServiceCollection AddCustomDbContext(this IServiceCollection services)
{
    // Add framework services.

    return services;
}

public static IServiceCollection RegisterCustomServices(this IServiceCollection services)
{
    // New instance every time, only configuration class needs so its ok
    services.Configure<SmsSettings>(options => Startup.Configuration.GetSection("SmsSettingsTwillio").Bind(options));
    services.AddTransient<UserResolverService>();
    services.AddTransient<IEmailSender, EmailSender>();
    services.AddTransient<ISmsSender, SmsSender>();
    services.AddScoped<ApiExceptionFilter>();
    return services;
}

Here my packages:

<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Introspection" Version="2.0.0-*" />
<PackageReference Include="AspNet.Security.OAuth.Validation" Version="2.0.0-*" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.AzureAppServicesIntegration" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Cors" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Antiforgery" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Twitter" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.AngularServices" Version="1.1.0-beta-000002" />
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="1.0.0-beta3-final" />
<PackageReference Include="AspNet.Security.OAuth.LinkedIn" Version="1.0.0-beta3-final" />
<PackageReference Include="OpenIddict" Version="2.0.0-*" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="2.0.0-*" />
<PackageReference Include="OpenIddict.Mvc" Version="2.0.0-*" />
<PackageReference Include="SendGrid" Version="9.9.0" />
<PackageReference Include="MailKit" Version="1.18.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="1.0.0" />
<PackageReference Include="Twilio" Version="5.6.3" />
<PackageReference Include="Stripe.net" Version="10.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="Webpack" Version="4.0.0" />
<PackageReference Include="Serilog" Version="2.5.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.Sinks.Seq" Version="3.3.3" />
<PackageReference Include="Bogus" Version="17.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0">
  <PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0">
  <PrivateAssets>All</PrivateAssets>
</PackageReference>

  <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.ViewCompilation" Version="2.0.0" PrivateAssets="All" />
  </ItemGroup>

  <ItemGroup>
<DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
  </ItemGroup>

Here are my logs:

info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
  Request starting HTTP/1.1 GET http://localhost:44331/api/profile/test 
application/json; charset=UTF-8 
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
  Authorization failed for user: (null).
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
  Authorization failed for user: (null).
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
  Authorization failed for the request at filter 
'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
  Authorization failed for the request at filter 
'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
  Executing ChallengeResult with authentication schemes ().
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
  Executing ChallengeResult with authentication schemes ().
info: 
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12]
  AuthenticationScheme: Identity.Application was challenged.
info: 
Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[12]
  AuthenticationScheme: Identity.Application was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
  Executed action 
AspNetCoreSpa.Server.Controllers.api.ProfileController.Test (AspNetCoreSpa) 
in 43.3105ms
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
  Executed action 
AspNetCoreSpa.Server.Controllers.api.ProfileController.Test (AspNetCoreSpa) 
in 43.3105ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
  Request finished in 67.4133ms 302 
infoinfo: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
  Request finished in 67.4133ms 302 
: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
  Request starting HTTP/1.1 GET http://localhost:44331/Account/Login?
ReturnUrl=%2Fapi%2Fprofile%2Ftest application/json; charset=UTF-8 
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[1]
  Request starting HTTP/1.1 GET http://localhost:44331/Account/Login?
ReturnUrl=%2Fapi%2Fprofile%2Ftest application/json; charset=UTF-8 
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
  Executing action method 
AspNetCoreSpa.Server.Controllers.HomeController.Index (AspNetCoreSpa) with 
arguments ((null)) - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
  Executing action method 
AspNetCoreSpa.Server.Controllers.HomeController.Index (AspNetCoreSpa) with 
arguments ((null)) - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1]
  Executing ViewResult, running view at path /Views/Home/Index.cshtml.
info: Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.ViewResultExecutor[1]
  Executing ViewResult, running view at path /Views/Home/Index.cshtml.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
  Executed action AspNetCoreSpa.Server.Controllers.HomeController.Index 
(AspNetCoreSpa) in 13.2746ms
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
  Executed action AspNetCoreSpa.Server.Controllers.HomeController.Index 
(AspNetCoreSpa) in 13.2746ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
  Request finished in 79.2352ms 200 text/html; charset=utf-8
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
  Request finished in 79.2352ms 200 text/html; charset=utf-8

I wonder about the following line:

Authorization failed for user: (null)

Already found this Authorization failed for user: (null) but there is no answer yet and I think it's a .NET Core 1 issue.

ekad
  • 13,718
  • 26
  • 42
  • 44
Simon Arendt
  • 111
  • 1
  • 1
  • 5

3 Answers3

11

When you call AddIdentity, it adds Cookie authentication, which overrides your intended JWT bearer authentication. One way to get around this is to move AddIdentity call before setting up JWT authentication. The following is the code that works for me:

// setup identity
services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<MyMoneyDbContext>()
    .AddDefaultTokenProviders();

// setup Jwt authentication
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
{
    jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        ...

Another alternative is to use AddIdentityCore but I never tried that.

Codism
  • 5,002
  • 5
  • 25
  • 28
8

I encountered the same issue and in order to solve the problem I had to include the authentication scheme in the Authorize attribute on the controller.

In your case:

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ServiceFilter(typeof(ApiExceptionFilter))]
[ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
public class BaseController : Controller
{
    public BaseController()
    {
    }
}
mjegen
  • 81
  • 3
0

For me, it was related to Cross-Origin Resource Sharing (CORS). I have an API in an App Service in Azure with CORS enabled. I got the 302 to disappear when I added the callers of the API to the list of allowed origins.

I had to do this even though I had already added these origins in my aspnet core startup code.

Kristoffer
  • 834
  • 5
  • 22
  • This is not how it should be working right? As per MSFT, if we dont have anything in the cors section in azure app service, it should take the values from the code. I am also struck at the same point now. – Alok Rajasukumaran Jan 28 '20 at 15:20