I am experiencing a strange issue, it's a bit complex, but I'll do my best to explain it.
I have a CMS which runs on ASP.Net Core 2.2. I've lately added to the cookie authentication also Bearer authentication with JWT. The Startup.cs
file looks like this:
services.AddAuthentication()
.AddCookie(cfg =>
{
cfg.SlidingExpiration = true;
cfg.ExpireTimeSpan = TimeSpan.FromMinutes(20);
cfg.LoginPath = new PathString("/account/login");
cfg.AccessDeniedPath = new PathString("/Account/AccessDenied");
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
[...]
services.ConfigureApplicationCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
options.SlidingExpiration = true;
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder() //makes every controller an every action requiring authorization except if labled by [AllowAnnonymous]
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.AddViewLocalization(
options => { options.ResourcesPath = "Resources"; })
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
So, as you see, cookie authentication is the main use, and every function which I want to use over the Bearer API, I annotate as follows:
[HttpGet]
[Produces("application/json")]
[Route("api/[controller]/[action]/")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public ActionResult GetUserDetails()
{
...
}
This works well for a few months already from different .NET applications and of course from postman. But now I want to connect an Android app (using Java and retrofit) and none of the functions work, even though I pass a valid token as Authorization
parameter in the HTTP request. As soon as I remove the Authorize
annotation and replace it with AllowAnonymous
it works and here comes the strange part: I can see the Authorization
parameter in the request header.
Here some screenshots:
Function called from Postman with Authorization
active - returning successfully:
Function called from android app with Authorization
active. The controller action is not even being hit but instead the request is redirected to the login page of the CMS (BTW, same thing happens, when I take the token from the postman to the android app and run the request with the very same token which I use for postman):
Now when I remove the Authorize
attribute on the controller and add a AllowAnonymous
instead, it works like a charm, but I can see that even though the header contains the Authorization
attribute, the user shows up as unauthenticated:
I am stuck on this for hours already, I tried to change a few things in the Startup.cs
file, I read a ton of articles about retrofit, oAuth2, Bearer etc. but unfortunately couldn't find any help so far.
So to me this behavior looks quite strange and my question is, whether I am missing something here, any special information in the header except for the Authorization
or whatever?
Any input would be much appreciated.
Here is the code of the Java (Android) app:
//Endpoint
public interface TradeEndPoint {
@Headers({ "Content-Type: application/json;charset=UTF-8"})
@GET("Trade/getAllActiveItemsApi")
Call<List<TradeItem>> getAllActiveItems(@Header("Authorization") String authHeader);
}
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(baseUrl.getStringValue())
.addConverterFactory(GsonConverterFactory.create())
.build();
TradeEndPoint te = retrofit.create(TradeEndPoint.class);
final Call<List<TradeItem>> getItems = te.getAllActiveItems("Bearer " + getAccessToken(context));
getItems.enqueue(new Callback<List<TradeItem>>() {
@Override
public void onResponse(Call<List<TradeItem>> call, Response<List<TradeItem>> response) {
TradeItemsLoaded til = (TradeItemsLoaded) fragment;
til.onTradeItemsLoaded(response);
}
@Override
public void onFailure(Call<List<TradeItem>> call, Throwable t) {
t.printStackTrace();
}
});
The function getAccessToken(context)
returns the token as a string.
Here is a screenshot of the fiddler session.
This is from the android app
This is from Postman (or an equivalent extension for Firefox called RESTer)
So yes, there are differences in the header, but which of them do matter, if any?
I've found the solution.
In the Startup.cs
file there is the following expression:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
})
.AddViewLocalization(
options => { options.ResourcesPath = "Resources"; })
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
Turns out that the authorization filter does work when working with postman and .NET, but for some reason makes trouble when Bearer authentication is being used from an android app.
I removed this, now it looks like this:
services.AddMvc()
.AddViewLocalization(
options => { options.ResourcesPath = "Resources"; })
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
Now I need to annotate every controller with [Authorize]
but at least it works (maybe you could get the authorization filter work with android as well, but I am not using it anyways, so it's not much of a big deal).