I'm struggling to redirect a user to the correct location in an ASP.NET Core middleware.
What I'm trying to archieve is an application relative redirect, so if my app runs as https://www.example.com/app any link generated should be rooted there.
E.g. ~/path should resolve to /app/path.
I tried using UrlHelperFactory, but that's MVC and needs an ActionContext, which does not exist inside a middleware.
Heres a code sample of my middleware:
app.Map(pathMatch, appMap =>
{
// [... more code ...]
appMap.Use((context, next) =>
{
var nextUrl = context.Request.Query["nextUrl"];
var applicationBase = context.Request.PathBase;
if (nextUrl == StringValues.Empty || !UrlUtil.IsLocalUrl(nextUrl[0]))
{
context.Response.Redirect(applicationBase);
}
else
{
context.Response.Redirect(applicationBase.Add(nextUrl[0]));
}
return Task.CompletedTask;
});
});
Related
When a request is made for an image|file it doesn't reach my middleware.
I can see that when UseCms is called it adds PiranhaStartupFilter
serviceBuilder.Services.AddTransient<IStartupFilter, PiranhaStartupFilter>();
Which adds UseStaticFiles() to the ApplicationBuilder.
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder
.UseSecurityMiddleware()
.UseStaticFiles()
.UseMiddleware<RoutingMiddleware>()
.UseMiddleware<SitemapMiddleware>();
next(builder);
};
}
How could I overwrite this functionality so that my middleware is called for requests?
I'm expecting a call to /uploads/foo.jpg would be picked up in the InvokeAsync method of my middleware, registered like so:
app.UsePiranha(options =>
{
options.Builder.CustomImageTools();
});
At present only files & favicon requests reach the InvokeAsync method in my middleware.
As middleware in asp.net is executed in the order they are added into the pipeline it should be sufficient to add your interceptor before making any calls to Piranha.
You can add the middleware by adding a service above the call to UseCms() in startup.cs.
services.AddPiranha(options =>
{
options.CustomImageFilter();
options.UseCms();
}
public class CustomImageFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.UseWhen(
context => some_condition,
appbuilder => appbuilder.CustomImageTools()
);
// Call the next configure method
next(app);
};
}
}
I am attempting to access a route defined in my routes/web.php file:
Route::get('/dashboard', [ConsoleController::class, 'dashboard'])->middleware('auth');
I am not logged in. The Authenticate.php middleware file attempts to redirect me back to the login page:
class Authenticate extends Middleware
{
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('');
}
}
}
I have also tried using return route('/'); in the Authenticate.php middleware.
My routes/web.php file has a default route which works fine if I go to the page manually:
Route::get('/', [ConsoleController::class, 'loginForm'])->middleware('guest');
However, the Authenticate.php is causing the following error:
Symfony\Component\Routing\Exception\RouteNotFoundException
Route [] not defined.
http://localhost:8888/dashboard
And it points to the following line of code:
public function route($name, $parameters = [], $absolute = true)
{
if (! is_null($route = $this->routes->getByName($name))) {
return $this->toRoute($route, $parameters, $absolute);
}
throw new RouteNotFoundException("Route [{$name}] not defined.");
}
I have found many similar posts on and off Stack Overflow, but none of those solutions have helped.
Am I naming my default route wrong? Can I not use this route in my Authenticate.php middleware? Any help would be appreciated.
Issue is, you are using route() method of Laravel, which expect route name as a parameter but you are passing actual url.
In your routes/web.php file, add name to your route as
Route::get('/dashboard', [ConsoleController::class, 'dashboard'])->middleware('auth')->name('dashboard');
Then in your Authenticate middleware file,
class Authenticate extends Middleware
{
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('dashboard');
}
}
}
Hello I have deployed my Vue.js app to Cloudflare workers using the following commands:
wrangler generate --site
wrangler publish --env dev
This is my wrangler.toml:
account_id = "xxx"
name = "name"
type = "webpack"
workers_dev = true
[site]
bucket = "./dist"
entry-point = "workers-site"
[env.dev]
name = "name"
route = "xxx.com/*"
zone_id = "XXX"
account_id = "XXX"
The website is fine and live on "xxx.com" but when I refresh the page on any other route I get this error message:
could not find es-es/index.html in your content namespace
Or for example:
could not find category/65/index.html in your content namespace
On nginx I had to create a .htaccess, but I have no idea on how to make it work here.
This is my index.js in case it helps:
import { getAssetFromKV, mapRequestToAsset } from '#cloudflare/kv-asset-handler'
/**
* The DEBUG flag will do two things that help during development:
* 1. we will skip caching on the edge, which makes it easier to
* debug.
* 2. we will return an error message on exception in your Response rather
* than the default 404.html page.
*/
const DEBUG = false
addEventListener('fetch', event => {
try {
event.respondWith(handleEvent(event))
} catch (e) {
if (DEBUG) {
return event.respondWith(
new Response(e.message || e.toString(), {
status: 500,
}),
)
}
event.respondWith(new Response('Internal Error', { status: 500 }))
}
})
async function handleEvent(event) {
const url = new URL(event.request.url)
let options = {}
/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
// options.mapRequestToAsset = handlePrefix(/^\/docs/)
try {
if (DEBUG) {
// customize caching
options.cacheControl = {
bypassCache: true,
}
}
return await getAssetFromKV(event, options)
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
})
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
} catch (e) {}
}
return new Response(e.message || e.toString(), { status: 500 })
}
}
/**
* Here's one example of how to modify a request to
* remove a specific prefix, in this case `/docs` from
* the url. This can be useful if you are deploying to a
* route on a zone, or if you only want your static content
* to exist at a specific path.
*/
function handlePrefix(prefix) {
return request => {
// compute the default (e.g. / -> index.html)
let defaultAssetKey = mapRequestToAsset(request)
let url = new URL(defaultAssetKey.url)
// strip the prefix from the path for lookup
url.pathname = url.pathname.replace(prefix, '/')
// inherit all other props from the default request
return new Request(url.toString(), defaultAssetKey)
}
}
As you know, Vue.js (like many other SPA frameworks) expects that for any path that doesn't map to a specific file, the server falls back to serving the root /index.html file. Vue will then do routing in browser-side JavaScript. You mentioned that you know how to accomplish this fallback with .htaccess, but how can we do it with Workers?
Good news: In Workers, we can write code to do whatever we want!
In fact, the worker code already has a specific block of code to handle "404 not found" errors. One way to solve the problem would be to change this block of code so that instead of returning the 404 error, it returns /index.html.
The code we want to change is this part:
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/404.html`, req),
})
return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 })
} catch (e) {}
}
return new Response(e.message || e.toString(), { status: 500 })
}
We want to change it to:
} catch (e) {
// Fall back to serving `/index.html` on errors.
return getAssetFromKV(event, {
mapRequestToAsset: req => new Request(`${new URL(req.url).origin}/index.html`, req),
})
}
That should do the trick.
However, the above solution has a slight problem: For any HTML page (other than the root), it will do two lookups, first for the specific path, and only after that will it look for /index.html as a fallback. These lookups are pretty fast, but maybe we can make things faster by being a little bit smarter and detecting HTML pages upfront based on the URL.
To do this, we want to customize the mapRequestToAsset function. You can see a hint about this in a comment in the code:
/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
// options.mapRequestToAsset = handlePrefix(/^\/docs/)
Let's go ahead and use it. Replace the above comment with this:
options.mapRequestToAsset = req => {
// First let's apply the default handler, which we imported from
// '#cloudflare/kv-asset-handler' at the top of the file. We do
// this because the default handler already has logic to detect
// paths that should map to HTML files, for which it appends
// `/index.html` to the path.
req = mapRequestToAsset(req)
// Now we can detect if the default handler decided to map to
// index.html in some specific directory.
if (req.url.endsWith('/index.html')) {
// Indeed. Let's change it to instead map to the root `/index.html`.
// This avoids the need to do a redundant lookup that we know will
// fail.
return new Request(`${new URL(req.url).origin}/index.html`, req)
} else {
// The default handler decided this is not an HTML page. It's probably
// an image, CSS, or JS file. Leave it as-is.
return req
}
}
Now the code detects specifically HTML requests and replaces them with the root /index.html, so there's no need to waste time looking up a file that doesn't exist only to catch the resulting error. For other kinds of files (images, JS, CSS, etc.) the code will not modify the filename.
There appears to be a built-way to do this now:
import { getAssetFromKV, serveSinglePageApp } from '#cloudflare/kv-asset-handler'
...
let asset = await getAssetFromKV(event, { mapRequestToAsset: serveSinglePageApp })
https://github.com/cloudflare/kv-asset-handler#servesinglepageapp
We have an angular 5 application built on top of MVC authentication. The application is served up from the Home/Index action, and once the application is loaded, the angular routing takes care of pretty much everything. We are doing this primarily because we wanted to use MVC's Authentication processes (we are using Identity Server 4 as our Oath system).
This works well with one exception: logout. When we attempt to logout, the application seems to be immediately reauthorized and reloads instead of returning us to our Identity Server login page.
Originally, we had success in our development environment through this code:
[HttpPost]
public async Task<IActionResult> Logout()
{
foreach (string key in Request.Cookies.Keys)
{
Response.Cookies.Delete(key);
}
await HttpContext.SignOutAsync();
return Ok();
}
But it was a false positive, because all of our applications were running on localhost, so they had access to each other's cookies. Due to this, the Identity Server cookies were cleared along with the cookies for the Angular application.
We attempted to replace that logic with something like this:
public async Task Logout()
{
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await HttpContext.SignOutAsync("Cookies");
await HttpContext.SignOutAsync("oidc");
}
}
However, it did not log out in either environment (however that code works for one of our MVC applications using the same identity server).
To give some background, our logout process for the Angular application comes from a material menu, where we then prompt the user if they really want to logout with a modal, before calling the logout function. Code snippets of this process:
Method called by logout button:
public openDialog(): void {
let dialogRef = this.dialog.open(ModalComponent, {
width: '250px',
data: { text: this.logoutText, ok: this.okText, cancel: this.cancelText }
});
dialogRef.afterClosed().subscribe(result => {
if (result !== undefined) {
switch (result.data) {
case 'ok':
this.logout();
break;
case 'cancel':
break;
default:
break;
}
}
});
}
private logout(): void {
this.sessionService.logOut();
}
Session service:
public logOut(): void {
this.auth.revokeToken();
this.http.post(this.logoutUrl, undefined).subscribe(x => window.location.reload());
}
As mentioned, calling logout this way ends up with a page refresh and the user not really being logged out. There's probably something simple we are missing, but all of the tinkering I've done so far has not led to any success.
EDIT:
Our angular routing is fairly simple (though the pathing prevents us from calling something like home/logout):
{
path: 'parts',
component: PartsComponent,
canActivate: [AuthGuard],
runGuardsAndResolvers: 'always',
data: { title: 'Parts' }
},
{
path: '',
redirectTo: 'parts',
pathMatch: 'full'
},
{
path: '**',
redirectTo: 'parts'
}
Our MVC routes are also fairly straightforward
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
"Sitemap",
"sitemap.xml",
new { controller = "Home", action = "SitemapXml" });
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
I'm not certain how we could easily route to home/logout with the angular routes the way they are. I'm guessing we'd have to add a route for it. We tried that at one point but it never routed correctly. I'm trying to find my notes on what we tried.
After you cleared the local cookies of your application send the user to the end_session_endpoint of your Idsrv. (The code you showed to clear your session should work, if not I'd the config in startup.cs and debug to check if the redirect really removed the cookie in the browser).
E.g. https://demo.identityserver.io/.well-known/openid-configuration
There you see the endpoint. This should remove the session on the Idsrv. Depending on your setup this could kill your sessions on your other applications that uses the same Identity Server instance.
You can read more about this in the docs.
I don't see a problem with the Angular routing interfering with your server side routing. As long as you ofcourse really do a redirect to that page using a regular window.location.replace
And ofcourse like #Win mentioned it would be a good security guideline to revoke refresh_token and reference_tokens if you use those.
I could not answer for Angular; I'm still working on it. However, client web app could ask IDP to revoke access token and refresh token when user signs out at client. For example, in ASP.Net Core -
using System;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace MY_APP.Controllers
{
public class AccountController : Controller
{
[HttpGet]
public async Task<IActionResult> SignOut()
{
var discoveryClient = new DiscoveryClient("IDP_URL");
var metaDataResponse = await discoveryClient.GetAsync();
var revocationClient = new TokenRevocationClient(
metaDataResponse.RevocationEndpoint,
"CLIENT_NAME",
"CLIENT_SECRET");
// revoke the access token
string accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenResponse = await revocationClient.RevokeAccessTokenAsync(accessToken);
if (revokeAccessTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the access token.",
revokeAccessTokenResponse.Exception);
}
// revoke the refresh token
string refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
if (!string.IsNullOrWhiteSpace(refreshToken))
{
var revokeRefreshTokenResponse = await revocationClient.RevokeAccessTokenAsync(refreshToken);
if (revokeRefreshTokenResponse.IsError)
throw new Exception("Problem encountered while revoking the refresh token.",
revokeRefreshTokenResponse.Exception);
}
return SignOut(
new AuthenticationProperties { RedirectUri = "CALL_BACK_URL" },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
}
}
We want to have 2 sets of resources for our AngularJS app (public/private) which uses RequireJS for dependency management. Basically everything on the login page would be public and once logged in, another angularjs app would be loaded (new requirejs config) that would load resources that require authentication to access.
Is there a way to configure requirejs to set an authorization header when loading resources?
It depends on what you mean by "resources" and how your server is configured. But in general - yes, since you are using AngularJS you can use the $httpProvider to inject an interceptor service.
For example, in a service:
var dependencies = ['$rootScope', 'userService'];
var service = function ($rootScope, userService) {
return {
request: function(config) {
var currentUser = userService.getCurrentUser();
var access_token = currentUser ? currentUser.access_token : null;
if(access_token) {
config.headers.authorization = access_token;
}
return config;
},
responseError: function (response) {
if(response.status === 401) {
$rootScope.$broadcast('unauthorized');
}
return response;
}
};
};
module.factory(name, dependencies.concat(service));
Then, after you configure your routes, you can use:
$httpProvider.interceptors.push( 'someService');
You can find some more information on interceptors here: https://docs.angularjs.org/api/ng/service/$http#interceptors
UPDATE
You might be able to use the text plugin to try and receive it, but I don't see the point in protecting client side code. Plus, if you want to use optimization the resources will just come in one file anyway...
config: {
text: {
onXhr: function (xhr, url) {
xhr.setRequestHeader('Authorization','Basic ' + token);
}
}
}
Refer to: custom-xhr-hooks
Another UPDATE
You could also use urlArgs (mainly used for cache invalidation) without using the text plugin:
require.config({
urlArgs: 'token='+token,
...
)}