I am currently trying to develop an authentication system to log in a user and give permission to access an API.
The API only has a controller that returns all the claims the user has:
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
On my API I have set up the JWT authentication and authorization:
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("database_conn");
// Identity
builder.Services.AddDbContext<MyDBContext>(opt =>
{
opt.UseNpgsql(connectionString);
});
builder.Services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<MyDBContext>()
.AddDefaultTokenProviders();
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters.ValidateAudience = false;
options.IncludeErrorDetails = true;
});
builder.Services.AddAuthorization(options =>
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
})
);
builder.Services.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.AllowAnyOrigin();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseCors("default");
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
When I try to reach the /identity endpoint after succesfully authenticating the user, I am redirected to localhost:6001/Account/Login which doesn't exist, it should be redirected to localhost:5001/Account/Login if there is no user authenticated.
The client I am using has the proper redirect links:
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RedirectUris = { "https://localhost:5003/callback.html" },
PostLogoutRedirectUris = { "https://localhost:5003/index.html" },
AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api1"
},
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Sliding
}
The Duende Server is getting back a cookie and a JWT token that contain all the claims that should be necessary to validate if the user is authenticated or not.
{
"alg": "RS256",
"kid": "DA4EE3153F56DF0877C3DDE4766DAB7B",
"typ": "at+jwt"
},
{
"iss": "https://localhost:5001",
"nbf": 1705681216,
"iat": 1705681216,
"exp": 1705684816,
"aud": "https://localhost:5001/resources",
"scope": [
"openid",
"profile",
"api1"
],
"amr": [
"pwd"
],
"client_id": "js",
"sub": "some-user-id",
"auth_time": 1705681215,
"idp": "local",
"sid": "04830986C58DCDD600C25B1CB0A93093",
"jti": "43EF5683A3ACA0041852B326F6620A9A"
}
App.js
/// <reference path="oidc-client.js" />
function log() {
document.getElementById("results").innerText = "";
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
} else if (typeof msg !== "string") {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById("results").innerText += msg + "\r\n";
});
}
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("api").addEventListener("click", api, false);
document.getElementById("logout").addEventListener("click", logout, false);
var config = {
authority: "https://localhost:5001",
client_id: "js",
redirect_uri: "https://localhost:5003/callback.html",
response_type: "code",
scope: "openid profile api1",
post_logout_redirect_uri: "https://localhost:5003/index.html",
};
var mgr = new Oidc.UserManager(config);
mgr.events.addUserSignedOut(function () {
log("User signed out of IdentityServer");
});
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
} else {
log("User not logged in");
}
});
function login() {
mgr.signinRedirect();
}
function api() {
mgr.getUser().then(function (user) {
console.log("user:", user)
fetch('https://localhost:6001/identity', {
method: 'GET',
headers: {
'Authorization': "Bearer " + user.access_token,
},
})
.then(response => console.log(response))
.catch(err => console.error(err));
});
}
function logout() {
mgr.signoutRedirect();
}
I do not see what I did wrong to get a different redirect than I was expecting and why the API does not recognize that the user is authenticated and has the proper authorization to get back info from it.
Your problem is that ASP.NET Identity is mixed with JwtBearer, and ASP.NET Identity takes over the request before JwtBearer has a chance to process it. My recommendation is to review the order of the middlewares in the request pipeline.
The only status codes you should see from the JWtBearer itself are 401 and 403. If you get redirects, then it is not from JwtBearer.