Custom header response when 401 /403 eror in .net 6/7 - asp.net-core

This is my code:
i am refernce this link:https://stackoverflow.com/a/71170460/7273263
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>{
o.RequireHttpsMetadata = true;
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{ ValidateIssuerSigningKey = true,
...other code
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
Newly Added code:
// Other configs...
o.Events = new JwtBearerEvents
{
OnChallenge = async context =>
{
// Call this to skip the default logic and avoid using the default response
var s = context.HttpContext.Response.StatusCode;
//***Here i am getting 200 error response***
context.HandleResponse();
var httpContext = context.HttpContext;
var statusCode = StatusCodes.Status401Unauthorized;
var routeData = httpContext.GetRouteData();
var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());
var factory = httpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
var problemDetails = factory.CreateProblemDetails(httpContext, statusCode);
var result = new ObjectResult(problemDetails) { StatusCode = statusCode };
await result.ExecuteResultAsync(actionContext);
}
};
The above code working fine .. but i need to response with 401 & 403 error How to dynamically add status code based on http code..if i hard coded 401 i am getting result as expected but it should work for 401 & 403 Please let me know is it possible or not
EDIT:
Controler method
[Authorize(Permissions.Master.Read)]

401 =>OnChallenge
OnChallenge = async context =>
{
context.HandleResponse();
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync(JsonConvert.SerializeObject(new ErrorResponse()
{
Errors = new List<KeyValuePair<string, IEnumerable<string>>>
{
new KeyValuePair<string, IEnumerable<string>>(nameof(HttpStatusCode.Unauthorized),
new[] { "Your login has expired, please login again" })
}
}, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
}));
}
403 => OnForbidden
OnForbidden = async context =>
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsync(JsonConvert.SerializeObject(new ErrorResponse()
{
Errors = new List<KeyValuePair<string, IEnumerable<string>>>
{
new KeyValuePair<string, IEnumerable<string>>(nameof(HttpStatusCode.Forbidden),
new[] { "Access denied" })
}
}, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
}));
},
full code
options.Events = new JwtBearerEvents
{
OnForbidden = async context =>
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsync(JsonConvert.SerializeObject(new ErrorResponse()
{
Errors = new List<KeyValuePair<string, IEnumerable<string>>>
{
new KeyValuePair<string, IEnumerable<string>>(nameof(HttpStatusCode.Forbidden),
new[] { "Access denied" })
}
}, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
}));
},
OnChallenge = async context =>
{
context.HandleResponse();
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync(JsonConvert.SerializeObject(new ErrorResponse()
{
Errors = new List<KeyValuePair<string, IEnumerable<string>>>
{
new KeyValuePair<string, IEnumerable<string>>(nameof(HttpStatusCode.Unauthorized),
//new[] { context?.ErrorDescription ?? "Unauthenticated request" })
new[] { "Your login has expired, please login again" })
}
}, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
}));
}
};

Related

HttpContext.User.Identity.IsAuthenticated is false after calling HttpContext.SignInAsync("schema-name", claimsPrincipal);

I am using multiple authentication schemes. Here is my Startup.cs code -
public void ConfigureServices(IServiceCollection services)
{
//code
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Default";
options.DefaultSignInScheme = "Default";
options.DefaultChallengeScheme = "O365OpenId";
})
.AddCookie("Default",options =>
{
options.LoginPath = "/";
options.LogoutPath = "/Logout";
options.Cookie.Name = "ip" + guid.ToString();
options.AccessDeniedPath = "/Auth/Denied";
})
.AddCookie("External", options =>
{
options.LoginPath = "/Auth/External";
options.LogoutPath = "/Logout";
options.Cookie.Name = "ip" + guid.ToString();
options.AccessDeniedPath = "/Auth/Denied";
})
.AddOAuth("O365OpenId", options =>
{
//options.Authority = "given";
options.ClientId = "given";
options.ClientSecret = "given";
options.CallbackPath = "/auth/callback";
options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
options.AuthorizationEndpoint += "?prompt=select_account";
options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
options.UserInformationEndpoint = "https://graph.microsoft.com/oidc/userinfo";
options.AccessDeniedPath = "/Auth/Denied";
options.Scope.Add("openid");
options.Scope.Add("email");
//options.Scope.Add("profile");
//options.Scope.Add("offline_access");
//options.Scope.Add("user.read");
//options.Scope.Add("calendars.read");
options.SaveTokens = false;
//options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
//options.ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
//options.ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
//options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
//options.ClaimActions.MapJsonKey(ClaimTypes., "picture");
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
if (context.AccessToken is { })
{
context.Identity?.AddClaim(new Claim("access_token", context.AccessToken));
}
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(json.RootElement);
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy =>
{
policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireRole("Admin");
});
options.AddPolicy("Regular&Admin", policy =>
{
policy.AuthenticationSchemes.Add(CookieAuthenticationDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
policy.RequireRole("Regular");
});
options.AddPolicy("External", policy =>
{
policy.AuthenticationSchemes.Add("External");
policy.RequireAuthenticatedUser();
policy.RequireRole("External");
});
});
}
When I sign in user using the "Default" scheme, I find that the HttpContext.User.Identity.IsAuthenticated is true after HttpContext.SignInAsync("schema-name", claimsPrincipal); method call. Here is the code for signing in -
public async Task<IActionResult> Success(string returnUrl)
{
// code
var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
await HttpContext.SignInAsync("Default",claimsPrincipal);
// more code
}
isAuthenticated is true
But When I sign in user using "External" scheme, HttpContext.User.Identity.IsAuthenticated is false after HttpContext.SignInAsync("schema-name", claimsPrincipal); call. Here is the code for signing in -
public async Task<IActionResult> External([FromQuery] string ReturnUrl, LoginOTP loginOTP, string email)
{
//code
var claimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
await HttpContext.SignInAsync("External", claimsPrincipal);
//code
}
IsAuthenticated is false.
I cannot understand what I am doing wrong. Why is the IsAuthenticated returning false?
Please help. Thank you.
you have 3 authentication schemes
the first one is cookie-based and you named it: "Default",
the second one is also cookie-based and you named it: "External",
the third one is OAhut-based and you named it: "O365OpenId",
when referring to one of these schemes you should use the names that you have created.
now the issue is, that you're creating a ClaimeIdentity with one scheme but you signing the user with a different scheme.
for the external method it should be like this:
var claimIdentity = new ClaimsIdentity(claims, "External");
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
// code ...
await HttpContext.SignInAsync("External", claimsPrincipal);
now the claims principal will be associated with the "External" scheme and the user will be authenticated.

Use JWT token in multiple projects

I have 3 projects JWT.IDP, JWT.API, JWT.MVC.
JWT.IDP - an API project validates user and issues the JWT token.
JWT.API - an API project for my business logic, CURD etc
JWT.MVC - an MVC application for UI.
My intention is to use this token generated in JWT.IDP and call the JWT.API functions from JWT.MVC
The IDP token is working perfectly fine, I can generate the token and my JWT.MVC Login controller is able to receive it. But when I am trying to use this token to access the JWT.API it gives a 500 error (Please see the last function in the below code (GetWeatherData)).
Can someone help, I am not an advanced user, the code written below is taken from several samples. So I am not sure whether it really is the right code.
namespace JWT.MVC.Controllers
{
public class LoginController : Controller
{
public IActionResult DoLogin()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DoLogin([Bind("EmailOrName,Password")] LoginRequestModel loginRequestModel)
{
var apiName = $"https://localhost:44318/api/User/login";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, loginRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<IEnumerable<AccessibleDb>>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
foreach (var item in data)
{
item.UserName = loginRequestModel.EmailOrName;
}
return View("SelectDatabase" , data);
}
public async Task<IActionResult> PostLogin(string db, string user)
{
TokenRequestModel tokenRequestModel = new TokenRequestModel() { Database = db, UserName = user };
var apiName = $"https://localhost:44318/api/User/tokenonly";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, tokenRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<AuthenticationModel>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var stream = data.Token;
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(stream);
var tokenS = jsonToken as JwtSecurityToken;
var selectedDb = tokenS.Claims.First(claim => claim.Type == "Database").Value;
ViewBag.SelectedDb = selectedDb;
return View(data);
}
public async Task<IActionResult> GetWeatherData(string token)
{
var apiName = $"https://localhost:44338/weatherforecast";
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await httpClient.GetAsync(apiName);
if (!response.IsSuccessStatusCode)
{
ViewBag.Error = response.StatusCode;
return View("Weatherdata");
}
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<WeatherForecast>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return View("Weatherdata" , data);
}
}
}
Startup class for JWT.MVC is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
Startup class for JWT.API is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Copy from IS4
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API", Version = "v1" });
});
}
Startup class for JWT.IDP is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Configuration from AppSettings
services.Configure<JwtSettings>(Configuration.GetSection("JWT"));
//User Manager Service
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>();
services.AddScoped<IUserService, UserService>();
//Adding DB Context with MSSQL
services.AddDbContext<IdentityDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDbConnectionString"),
b => b.MigrationsAssembly(typeof(IdentityDbContext).Assembly.FullName)));
//Adding Athentication - JWT
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(Convert.ToInt32(Configuration["JWT:DurationInMinutes"])),
ValidIssuer = Configuration["JWT:Issuer"],
ValidAudience = Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"]))
};
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.IDP", Version = "v1" });
});
}
And the JWT Setting is as below
"JWT": {
"key": "C1CF4B7DC4C4175B6618DE4F55CA4",
"Issuer": "http://localhost:44318",
"Audience": "SecureApiUser",
"DurationInMinutes": 60
},
It's quite surprising that no one was able to identify the mistake. I made the following changes and it works perfectly fine now.
The ConfigureServices is like below in both MVC and API projects. No other changes to any other codes.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var authenticationProviderKey = "IdentityApiKey";
var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("C1CF4B7DC4C4175B6618DE4F55CA4"));
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = "http://localhost:44318",
ValidateAudience = true,
ValidAudience = "SecureApiUser",
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
RequireExpirationTime = true,
};
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = authenticationProviderKey;
})
.AddJwtBearer(authenticationProviderKey, x =>
{
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = tokenValidationParameters;
});
//services.AddAuthentication(options =>
//{
// options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
// options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
//}).AddJwtBearer(options =>
//{
// options.Authority = "https://localhost:44318"; ;
// options.RequireHttpsMetadata = false;
// options.Audience = "SecureApiUser";
//});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API2", Version = "v1" });
});
}

Using JWT Token in Client API

I have an API in .NET5 uses JWTBearer to secure the API. Now I wanted to configure my client app to use the token generated from the api/gettoken. It works fine in swagger, but I don't know how to configure my MVC and API(consuming API) to use this token. Can somebody help by providing the configureservices and configure methods in the startup
To Glenn,
I have 3 projects JWT.IDP, JWT.API, JWT.MVC. JWT.IDP issues the token, my intention is to use that token in JWT.API and call the JWT.API function from JWT.MVC. The IDP is working perfectly, I can generate the token and my JWT.MVC Login controller is able to receive it. The last function in the below code (GetWeatherData) is coded according to the idea you have given. If I don't pass the token, I used to get 401 error, now I get 500 Internal Server Error
namespace JWT.MVC.Controllers
{
public class LoginController : Controller
{
public IActionResult DoLogin()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DoLogin([Bind("EmailOrName,Password")] LoginRequestModel loginRequestModel)
{
var apiName = $"https://localhost:44318/api/User/login";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, loginRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<IEnumerable<AccessibleDb>>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
foreach (var item in data)
{
item.UserName = loginRequestModel.EmailOrName;
}
return View("SelectDatabase" , data);
}
public async Task<IActionResult> PostLogin(string db, string user)
{
TokenRequestModel tokenRequestModel = new TokenRequestModel() { Database = db, UserName = user };
var apiName = $"https://localhost:44318/api/User/tokenonly";
HttpClient httpClient = new HttpClient();
HttpResponseMessage response = await httpClient.PostAsJsonAsync(apiName, tokenRequestModel);
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<AuthenticationModel>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
var stream = data.Token;
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadToken(stream);
var tokenS = jsonToken as JwtSecurityToken;
var selectedDb = tokenS.Claims.First(claim => claim.Type == "Database").Value;
ViewBag.SelectedDb = selectedDb;
return View(data);
}
public async Task<IActionResult> GetWeatherData(string token)
{
var apiName = $"https://localhost:44338/weatherforecast";
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
HttpResponseMessage response = await httpClient.GetAsync(apiName);
if (!response.IsSuccessStatusCode)
{
ViewBag.Error = response.StatusCode;
return View("Weatherdata");
}
var jasonString = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync<WeatherForecast>
(jasonString, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
return View("Weatherdata" , data);
}
}
}
Startup class for JWT.MVC is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
}
Startup class for JWT.API is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Copy from IS4
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Audience = "SecureApiUser";
options.Authority = "https://localhost:44318";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
//End
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.API", Version = "v1" });
});
}
Startup class for JWT.IDP is as below
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
//Configuration from AppSettings
services.Configure<JwtSettings>(Configuration.GetSection("JWT"));
//User Manager Service
services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<IdentityDbContext>();
services.AddScoped<IUserService, UserService>();
//Adding DB Context with MSSQL
services.AddDbContext<IdentityDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("IdentityDbConnectionString"),
b => b.MigrationsAssembly(typeof(IdentityDbContext).Assembly.FullName)));
//Adding Athentication - JWT
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = false;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(Convert.ToInt32(Configuration["JWT:DurationInMinutes"])),
ValidIssuer = Configuration["JWT:Issuer"],
ValidAudience = Configuration["JWT:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"]))
};
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWT.IDP", Version = "v1" });
});
}
And the JWT Setting is as below
"JWT": {
"key": "C1CF4B7DC4C4175B6618DE4F55CA4",
"Issuer": "http://localhost:44318",
"Audience": "SecureApiUser",
"DurationInMinutes": 60
},
The short answer is
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", [token])
Keep "Bearer" as it is, it's just a constant. Replace [token], with the base 64 encoded token value returned to us from the OAuth protocol you are using.

Authentication & Authorization - Token in HTTP request body

I am trying to create a custom authentication handler that will require the Bearer JWT in the body of an HTTP request, but I'd prefer not to create a whole new custom authorization. Unfortunately, the only thing I can do is read the HTTP request body, get the token from there and put it in the Authorization header of the request.
Is there a different, more efficient way to do it? All I managed is to find the default JwtBearerHandler implementation on GitHub but when I make some modifications, it can't read the principal properly.
Startup.cs:
services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
RequireExpirationTime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = ctx =>
{
if (ctx.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
ctx.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
public class AuthHandler : JwtBearerHandler
{
private readonly IRepositoryEvonaUser _repositoryUser;
private OpenIdConnectConfiguration _configuration;
public AuthHandler(IOptionsMonitor<JwtBearerOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IDataProtectionProvider dataProtection,
ISystemClock clock,
IRepositoryUser repositoryUser,
OpenIdConnectConfiguration configuration
)
: base(options, logger, encoder, dataProtection, clock)
{
_repositoryUser = repositoryUser;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
try
{
var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options);
await Events.MessageReceived(messageReceivedContext);
if (messageReceivedContext.Result != null)
{
return messageReceivedContext.Result;
}
token = messageReceivedContext.Token;
if (string.IsNullOrEmpty(token))
{
Request.EnableBuffering();
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, true, 10, true))
{
var jsonBody = reader.ReadToEnd();
var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
if (body != null)
{
token = body.Token;
}
Request.Body.Position = 0;
}
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
}
if (_configuration == null && Options.ConfigurationManager != null)
{
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}
var validationParameters = Options.TokenValidationParameters.Clone();
if (_configuration != null)
{
var issuers = new[] { _configuration.Issuer };
validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(issuers) ?? issuers;
}
List<Exception> validationFailures = null;
SecurityToken validatedToken;
foreach (var validator in Options.SecurityTokenValidators)
{
if (validator.CanReadToken(token))
{
ClaimsPrincipal principal; // it can't find this
try
{
principal = validator.ValidateToken(token, validationParameters, out validatedToken);
}
catch (Exception ex)
{
if (Options.RefreshOnIssuerKeyNotFound && Options.ConfigurationManager != null
&& ex is SecurityTokenSignatureKeyNotFoundException)
{
Options.ConfigurationManager.RequestRefresh();
}
if (validationFailures == null)
{
validationFailures = new List<Exception>(1);
}
validationFailures.Add(ex);
continue;
}
var tokenValidatedContext = new TokenValidatedContext(Context, Scheme, Options)
{
Principal = principal,
SecurityToken = validatedToken
};
await Events.TokenValidated(tokenValidatedContext);
if (tokenValidatedContext.Result != null)
{
return tokenValidatedContext.Result;
}
if (Options.SaveToken)
{
tokenValidatedContext.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = "access_token", Value = token }
});
}
tokenValidatedContext.Success();
return tokenValidatedContext.Result;
}
}
if (validationFailures != null)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = (validationFailures.Count == 1) ? validationFailures[0] : new AggregateException(validationFailures)
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
return AuthenticateResult.Fail(authenticationFailedContext.Exception);
}
return AuthenticateResult.Fail("No SecurityTokenValidator available for token: " + token ?? "[null]");
}
catch (Exception ex)
{
var authenticationFailedContext = new AuthenticationFailedContext(Context, Scheme, Options)
{
Exception = ex
};
await Events.AuthenticationFailed(authenticationFailedContext);
if (authenticationFailedContext.Result != null)
{
return authenticationFailedContext.Result;
}
throw;
}
}
}
Or, is there a way to just tell the application to expect a JWT in the HTTP request body? I am well aware that the token should be sent in the request header instead of body, but I am interested into seeing if (and if so, how) this can be implemented.
I also tried this:
OnMessageReceived = ctx =>
{
ctx.Request.EnableBuffering();
using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 10, true))
{
var jsonBody = reader.ReadToEnd();
var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
if (body != null)
{
ctx.Token = body.Token;
ctx.Request.Body.Position = 0;
}
}
return Task.CompletedTask;
}
By default , AddJwtBearer will get token from request header , you should write your logic to read token from request body and validate the token . That means no such configuration to "tell" middleware to read token form request body .
If token is sent in request body , you need to read the request body in middleware and put token in header before the jwt middleware reaches. Or read the request body in one of the jwt bearer middleware's event , for example , OnMessageReceived event , read token in request body and at last set token like : context.Token = token; . Here is code sample for reading request body in middleware .
I'll mark #Nan Yu's answer as the correct one, but I'll post my final code nonetheless. What I essentially did was revert back to the default JwtBearerHandler and use JwtBearerOptions and JwtBearerEvents's OnMessageReceived event to get the token value from HTTP request's body.
They all reside in the Microsoft.AspNetCore.Authentication.JwtBearer namespace.
services
.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
RequireExpirationTime = true,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = ctx =>
{
if (ctx.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
ctx.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
},
OnMessageReceived = ctx =>
{
ctx.Request.EnableBuffering();
using (var reader = new StreamReader(ctx.Request.Body, Encoding.UTF8, true, 1024, true))
{
var jsonBody = reader.ReadToEnd();
var body = JsonConvert.DeserializeObject<BaseRequest>(jsonBody);
ctx.Request.Body.Position = 0;
if (body != null)
{
ctx.Token = body.Token;
}
}
return Task.CompletedTask;
}
};
});

AddJwtBearer OnAuthenticationFailed return custom error

I am using Openidict.
I am trying to return custom message with custom status code, but I am unable to do it. My configuration in startup.cs:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = this.Configuration["Authentication:OpenIddict:Authority"];
o.Audience = "MyApp"; //Also in Auhorization.cs controller.
o.RequireHttpsMetadata = !this.Environment.IsDevelopment();
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
context.Response.StatusCode = HttpStatusCodes.AuthenticationFailed;
context.Response.ContentType = "application/json";
var err = this.Environment.IsDevelopment() ? context.Exception.ToString() : "An error occurred processing your authentication.";
var result = JsonConvert.SerializeObject(new {err});
return context.Response.WriteAsync(result);
}
};
});
But the problem is no content is returned. Chrome developer tools report
(failed)
for Status and
Failed to load response data
for response.
I also tried:
context.Response.WriteAsync(result).Wait();
return Task.CompletedTask;
but the result is the same.
Desired behaviour:
I would like to return custom status code with message what went wrong.
It's important to note that both the aspnet-contrib OAuth2 validation and the MSFT JWT handler automatically return a WWW-Authenticate response header containing an error code/description when a 401 response is returned:
If you think the standard behavior is not convenient enough, you can use the events model to manually handle the challenge. E.g:
services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = "http://localhost:54540/";
options.Audience = "resource_server";
options.RequireHttpsMetadata = false;
options.Events = new JwtBearerEvents();
options.Events.OnChallenge = context =>
{
// Skip the default logic.
context.HandleResponse();
var payload = new JObject
{
["error"] = context.Error,
["error_description"] = context.ErrorDescription,
["error_uri"] = context.ErrorUri
};
context.Response.ContentType = "application/json";
context.Response.StatusCode = 401;
return context.Response.WriteAsync(payload.ToString());
};
});
Was facing same issue, tried the solution provided by Pinpoint but it didnt work for me on ASP.NET core 2.0. But based on Pinpoint's solution and some trial and error, the following code works for me.
var builder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = "http://192.168.0.110/auth/realms/demo";
o.Audience = "demo-app";
o.RequireHttpsMetadata = false;
o.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = c =>
{
c.NoResult();
c.Response.StatusCode = 500;
c.Response.ContentType = "text/plain";
c.Response.WriteAsync(c.Exception.ToString()).Wait();
return Task.CompletedTask;
},
OnChallenge = c =>
{
c.HandleResponse();
return Task.CompletedTask;
}
};
});
This is what worked for me after finding issues related to this exception that seemed to appear after updating packages.
System.InvalidOperationException: StatusCode cannot be set because the response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowResponseAlreadyStartedException(String value)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.set_StatusCode(Int32 value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_StatusCode(Int32 value)
The implementation is below,
OnAuthenticationFailed = context =>
{
context.NoResult();
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
string response =
JsonConvert.SerializeObject("The access token provided is not valid.");
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
response =
JsonConvert.SerializeObject("The access token provided has expired.");
}
context.Response.WriteAsync(response);
return Task.CompletedTask;
},
OnChallenge = context =>
{
context.HandleResponse();
return Task.CompletedTask;
}
please check with the bellow code for .net core 2.1
OnAuthenticationFailed =context =>
{
context.Response.OnStarting(async () =>
{
context.NoResult();
context.Response.Headers.Add("Token-Expired", "true");
context.Response.ContentType = "text/plain";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Un-Authorized");
});
return Task.CompletedTask;
},
Below code work with .Net 6(minimal API
var app = builder.Build();
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) // 401
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new Error()
{
Message = "Token is not valid"
}));
}
});