I want to provide a custom reponse for all 404s on our API. For example:
{
"message": "The requested resource does not exist. Please visit our documentation.."
}
I believe the following result filter works for all cases within the MVC pipeline:
public class NotFoundResultFilter : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
var result = context.Result as NotFoundResult;
if (result != null)
{
context.Result = new HttpNotFoundResult(); // My custom 404 result object
}
}
}
But, when a URL requested does not match an action route, the above filter is not hit. How could I best intercept these 404 responses? Would this require middleware?
Yes, you need to use middleware, as filter is only for MVC.
You may, as always, write your own middleware
app.Use(async (context, next) =>
{
await next();
if (context.Response.StatusCode == 404)
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject("your text"), Encoding.UTF8);
}
});
Or use built-in middlware StatusCodePagesMiddleware, but as you want to handle only one status, this is an extra functionality. This middleware can be used to handle the response status code is between 400 and 600 .You can configure the StatusCodePagesMiddleware adding one of the following line to the Configure method (example from StatusCodePages Sample):
app.UseStatusCodePages(); // There is a default response but any of the following can be used to change the behavior.
// app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
// app.UseStatusCodePages("text/plain", "Response, status code: {0}");
// app.UseStatusCodePagesWithRedirects("~/errors/{0}"); // PathBase relative
// app.UseStatusCodePagesWithRedirects("/base/errors/{0}"); // Absolute
// app.UseStatusCodePages(builder => builder.UseWelcomePage());
// app.UseStatusCodePagesWithReExecute("/errors/{0}");
Try this:
app.Use( async ( context, next ) =>
{
await next();
if ( context.Response is { StatusCode: 404, Body: { Length: 0 }, HasStarted: false } )
{
context.Response.ContentType = "application/problem+json; charset=utf-8";
string jsonString = JsonConvert.SerializeObject(errorDTO);
await context.Response.WriteAsync( jsonString, Encoding.UTF8 );
}
} );
Related
I'm using Chopper in my flutter app and what I need to do is, when I get 401 response status code (unauthorized) from my API, I must call another endpoint that will refresh my token and save it into secured storage, when all of this is done, I need to retry the request instantly (so that user cannot notice that his token expired). Is this dooable with Chopper only, or I have to use some other package?
It is possible. You need to use the authenticator field on the Chopper client, e.g.
final ChopperClient client = ChopperClient(
baseUrl: backendUrl,
interceptors: [HeaderInterceptor()],
services: <ChopperService>[
_$UserApiService(),
],
converter: converter,
authenticator: MyAuthenticator(),
);
And your authenticator class, should look something like this:
class MyAuthenticator extends Authenticator {
#override
FutureOr<Request?> authenticate(
Request request, Response<dynamic> response) async {
if (response.statusCode == 401) {
String? newToken = await refreshToken();
final Map<String, String> updatedHeaders =
Map<String, String>.of(request.headers);
if (newToken != null) {
newToken = 'Bearer $newToken';
updatedHeaders.update('Authorization', (String _) => newToken!,
ifAbsent: () => newToken!);
return request.copyWith(headers: updatedHeaders);
}
}
return null;
}
Admittedly, it wasn't that easy to find/understand (though it is the first property of the chopper client mentioned in their docs), but it is precisely what this property is for. I was going to move to dio myself, but I still had the same issue with type conversion on a retry.
EDIT: You will probably want to keep a retry count somewhere so you don't end up in a loop.
I searched couple of days for answer, and I came to conclusion that this is not possible with Chopper... Meanwhile I switched to Dio as my Networking client, but I used Chopper for generation of functions/endpoints.
Here is my Authenticator. FYI I'm storing auth-token and refresh-token in preferences.
class AppAuthenticator extends Authenticator {
#override
FutureOr<Request?> authenticate(Request request, Response response, [Request? originalRequest]) async {
if (response.statusCode == HttpStatus.unauthorized) {
final client = CustomChopperClient.createChopperClient();
AuthorizationApiService authApi = client.getService<AuthorizationApiService>();
String refreshTokenValue = await Prefs.refreshToken;
Map<String, String> refreshToken = {'refresh_token': refreshTokenValue};
var tokens = await authApi.refresh(refreshToken);
final theTokens = tokens.body;
if (theTokens != null) {
Prefs.setAccessToken(theTokens.auth_token);
Prefs.setRefreshToken(theTokens.refresh_token);
request.headers.remove('Authorization');
request.headers.putIfAbsent('Authorization', () => 'Bearer ${theTokens.auth_token}');
return request;
}
}
return null;
}
}
Based on this example: github
And Chopper Client:
class CustomChopperClient {
static ChopperClient createChopperClient() {
final client = ChopperClient(
baseUrl: 'https://example.com/api/',
services: <ChopperService>[
AuthorizationApiService.create(),
ProfileApiService.create(),
AccountingApiService.create(), // and others
],
interceptors: [
HttpLoggingInterceptor(),
(Request request) async => request.copyWith(headers: {
'Accept': "application/json",
'Content-type': "application/json",
'locale': await Prefs.locale,
'Authorization': "Bearer ${await Prefs.accessToken}",
}),
],
converter: BuiltValueConverter(errorType: ErrorDetails),
errorConverter: BuiltValueConverter(errorType: ErrorDetails),
authenticator: AppAuthenticator(),
);
return client;
}
}
I just need advise if this is feasible. I am developing an authorization for my Shopify app and I need to somewhat store the access token from shopify auth for future verification of my front-end app.
So the first end-point the shopify is calling is this one:
[HttpGet("install")]
public async Task<IActionResult> Install()
{
try
{
if (ModelState.IsValid)
{
var queryString = Request.QueryString.Value;
var isValid = _shopifyService.VerifyRequest(queryString);
if (isValid)
{
var shopifyUrl = Request.Query["shop"];
var authUrl = _shopifyService.BuildAuthUrl(shopifyUrl,
$"{Request.Scheme}://{Request.Host.Value}/api/shopify/authorize",
Program.Settings.Shopify.AuthorizationScope);
return Redirect(authUrl);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
This works fine and the result of this api call will actually redirect to same link to my api, but this one will authorize the app:
[HttpGet("authorize")]
public async Task<IActionResult> AuthorizeStore()
{
try
{
if (ModelState.IsValid)
{
var code = Request.Query["code"];
var shopifyUrl = Request.Query["shop"];
var accessToken = await _shopifyService.AuthorizeStore(code, shopifyUrl).ConfigureAwait(false);
var identity = User.Identity as ClaimsIdentity;
identity.AddClaim(new Claim(Constants.Claims.AccessToken, accessToken));
// genereate the new ClaimsPrincipal
var claimsPrincipal = new ClaimsPrincipal(identity);
// store the original tokens in the AuthenticationProperties
var props = new AuthenticationProperties {
AllowRefresh = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(1),
IsPersistent = false,
IssuedUtc = DateTimeOffset.UtcNow,
};
// sign in using the built-in Authentication Manager and ClaimsPrincipal
// this will create a cookie as defined in CookieAuthentication middleware
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, props).ConfigureAwait(false);
Uri uri = new Uri($"{Program.Settings.Shopify.RedirectUrl}?token={accessToken}");
return Redirect(uri.ToString());
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
So the above api will authorize my app in shopify and will return an access token. The accessToken is the one I want to save in the claims identity with Cookie authentication type(this is without authorizing user credentials). Still no errors at that point and after calling the HttpContext.SignInAsync function, I can still view using debugger the newly added claims.
As, you can see in the code, after assigning claims, I call to redirect the app to front-end link(Note: front-end and back-end has different url)
In my front-end app, I have a Nuxt middleware that I put a logic to check the token received from back-end since I only pass the token to the front-end app using query params. Here's my middleware code:
export default function ({ app, route, next, store, error, req }) {
if (process.browser) {
const shopifyAccessToken = store.get('cache/shopifyAccessToken', null)
if (!shopifyAccessToken && route.query.token) {
// if has token on query params but not yet in cache, store token and redirect
store.set('cache/shopifyAccessToken', route.query.token)
app.router.push({
path: '/',
query: {}
})
// verify access token on the route
app.$axios
.get(`/shopify/verifyaccess/${route.query.token}`)
.catch((err) => {
error(err)
})
} else if (!shopifyAccessToken && !route.query.token) {
// if does not have both, throw error
error({
statusCode: 401,
message: 'Unauthorized access to this app'
})
}
} else {
next()
}
}
In my middleware, when the route has query params equal to token= It calls another api to verify the accessToken saved in my claims identity:
[HttpGet("verifyaccess/{accessToken}")]
public async Task<IActionResult> VerifyAccess(string accessToken)
{
try
{
if (ModelState.IsValid)
{
var principal = HttpContext.User;
if (principal?.Claims == null)
return Unauthorized(_localizer["NotAuthenticated"]);
var accessTokenClaim = principal.FindFirstValue(Constants.Claims.AccessToken);
if (accessToken == accessTokenClaim)
{
return Ok();
}
else
{
return Unauthorized(_localizer["NotAuthenticated"]);
}
}
}
catch (Exception ex)
{
var exceptionMessage = await ApiHelpers.GetErrors(ex, _localizer).ConfigureAwait(false);
ModelState.AddModelError(new ValidationResult(exceptionMessage));
}
ModelState.AddModelError(new ValidationResult(_localizer["InvalidAuthStore"]));
return BadRequest(ModelState.GetErrors());
}
Looking at the code above, it always fails me because the claims identity that I saved on the authorize endpoint was not there or in short the ClaimsIdentity is always empty.
Here's how I register the Cookie config:
private void ConfigureAuthCookie(IServiceCollection services)
{
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.RequireAuthenticatedSignIn = false;
})
.AddCookie(options => {
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
options.SlidingExpiration = true;
options.Cookie.Name = "shopifytoken";
});
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
}
and I also put a app.UseAuthentication() and app.UseAuthorization() on my Startup.Configure
Please let me know if this seems confusing so I can revised it. My main goal here is to be able to access that accessToken that I saved in the ClaimsIdentity so that I can verify the token. The reason why I did this because currently the shopify does not have an API for verifying access token. So when a user access my app link like this one http://example.com/?token=<any incorrect token> then they can already access my app.
I have this configured in my StartUp.cs:
public void ConfigureServices(IServiceCollection services)
{
services
.ConfigureEmail(Configuration)
.AddHealthChecksUI(setupSettings: setup =>
{
setup
.AddWebhookNotification("WebHookTest", "/WebhookNotificationError",
"{ message: \"Webhook report for [[LIVENESS]]: [[FAILURE]] - Description: [[DESCRIPTIONS]]\"}",
"{ message: \"[[LIVENESS]] is back to life\"}",
customMessageFunc: report =>
{
var failing = report.Entries.Where(e => e.Value.Status == UIHealthStatus.Unhealthy);
return $"{failing.Count()} healthchecks are failing";
},
customDescriptionFunc: report =>
{
var failing = report.Entries.Where(e => e.Value.Status == UIHealthStatus.Unhealthy);
return $"HealthChecks with names {string.Join("/", failing.Select(f => f.Key))} are failing";
});
})
.AddControllers();
}
public void Configure(IApplicationBuilder app)
{
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
app.UsePathBase(pathBase);
}
app.ConfigureExceptionHandler();
app
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapHealthChecksUI(options =>
{
options.ResourcesPath = string.IsNullOrEmpty(pathBase) ? "/ui/resources" : $"{pathBase}/ui/resources";
options.UIPath = "/hc-ui";
options.ApiPath = "/api-ui";
});
endpoints.MapDefaultControllerRoute();
});
}
And in the Controller:
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
public async Task<IActionResult> WebhookNotificationError([FromBody] string id)
{
MimeMessage mimeMessage = new MimeMessage { Priority = MessagePriority.Urgent };
mimeMessage.To.Add(new MailboxAddress(_configuration.GetValue<string>("ConfiguracionCorreoBase:ToEmail")));
mimeMessage.Subject = "WebHook Error";
BodyBuilder builder = new BodyBuilder { HtmlBody = id };
mimeMessage.Body = builder.ToMessageBody();
await _appEmailService.SendAsync(mimeMessage);
return Ok();
}
The watchdog application is configured in the appSettings.json to listen to different APIs.
So far everything works fine, but, if I force an error, I'd like to receive a notification email.
The idea is that, when an error occurs in any of the Healths, you send an email.
Environment:
.NET Core version: 3.1
Healthchecks version: AspNetCore.HealthChecks.UI 3.1.0
Operative system: Windows 10
It's look like you problem in routes. Did you verify that method with Postman?
Also check if your webHook request body is a text, try to change your template payload:
{ "message": "Webhook report for [[LIVENESS]]: [[FAILURE]] - Description: [[DESCRIPTIONS]]"}",
and in the controller change string to object. And check what you receive in DEBUG.
Try using Api/WebhookNotificationError inst. of /WebhookNotificationError if your controller is ApiController. The controller name seems to be missing
I think you should try this. It works for me.
[HttpPost]
[Consumes(MediaTypeNames.Application.Json)]
public async Task<IActionResult> WebhookNotificationError()
{
using (var reader = new StreamReader(
Request.Body,
encoding: Encoding.UTF8,
detectEncodingFromByteOrderMarks: false))
{
var payload = await reader.ReadToEndAsync();
//do whatever with your payloade here..
//I am just returning back for a example.
return Ok(payload);
}
}
I want to get a respond from Gmail of googleapis so I use a package:googleapis/gmail/v1.dart and make a code service.users.messages.list(_currentUser.email, q:'from:$searchingWord'); get list of email filtered by q:.
But there is a 400 Bad request, So, after debugging I found this url https://www.googleapis.com/gmail/v1/users/(myEmail)/messages?q=from%3Anoreply%40github.com&alt=json.
It's a bad request, so it's a url problem, but I'm not sure what's wrong.
I need your help.
The code where the problem occurred
Future<gMail.GmailApi> getGMailApi() async {
return gMail.GmailApi(await getGoogleClient());
}
Future<AuthClient> getGoogleClient() async {
return await clientViaServiceAccount(await getCredentials(), [
'email',
'https://mail.google.com/',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.readonly'
]);
}
Future<ServiceAccountCredentials> getCredentials() async {
if (credentials == null) {
credentials = ServiceAccountCredentials.fromJson(await rootBundle
.loadString('android/app/todoapp-270005-c7e7aeec11f7.json'));
}
print(credentials);
return credentials;
}
Future<void> handleGetMail(String searchingWord) async {
gMail.GmailApi service = (await getGMailApi());
gMail.ListMessagesResponse response =
await service.users.messages.list(_currentUser.email, q:'from:$searchingWord');
List<gMail.Message> messages;
while (response.messages != null) {
messages.addAll(response.messages);
if (response.nextPageToken != null) {
String pageToken = response.nextPageToken;
response = await service.users.messages
.list(_currentUser.email, q:'from:$searchingWord', pageToken: pageToken);
} else {
break;
}
}
for (gMail.Message message in messages) {
print(message.toString());
}
}
I am using asp.net core, and I would like to get several data from the request before I call the full web app.
So I created a middleware to do this. I found a way to check everything I want, but I don't know how to pass a variable to the following middlewares
app.Use(async (context, next) => {
var requestInfo = GetRequestInfo(context.Request);
if(requestInfo == null)
{
context.Response.StatusCode = 404;
return;
}
// How do I make the request info available to the following middlewares ?
await next();
});
app.Run(async (context) =>
{
// var requestInfo = ???
await context.Response.WriteAsync("Hello World! - " + env.EnvironmentName);
});
Is there a good way to pass data from a middleware to others ? (here I use app.Run, but I would like to have all this in MVC)
Beside features, there is another - a simpler in my opinion - solution: HttpContext.Items, as described here. According to the docs, it is especially designed to store data for the scope of a single request.
Your implementation would look like this:
// Set data:
context.Items["RequestInfo"] = requestInfo;
// Read data:
var requestInfo = (RequestInfo)context.Items["RequestInfo"];
I found the solution : the context contains an IFeatureCollection, and it is documented here
We just need to create a class with all the data :
public class RequestInfo
{
public String Info1 { get; set; }
public int Info2 { get; set; }
}
And we add it to the context.Features :
app.Use(async (context, next) => {
RequestInfo requestInfo = GetRequestInfo(context.Request);
if(requestInfo == null)
{
context.Response.StatusCode = 404;
return;
}
// We add it to the Features collection
context.Features.Set(requestInfo)
await next();
});
Now it is available to the others middlewares :
app.Run(async (context) =>
{
var requestInfo = context.Features.Get<RequestInfo>();
});