Interrupting application flow from a non-MVC event - zend-framework2

I'm working on an application which uses a REST API backend. This API has a login step which creates a token used for all subsequent API requests. I store this token in the auth storage, and I have an event hook that checks if the user is logged in, and if not, renders the login page:
$eventManager->attach(MvcEvent::EVENT_ROUTE, function($e) use ($view, $auth) {
$match = $e->getRouteMatch();
// No route match, this is a 404
if (!$match instanceof RouteMatch) {
return;
}
// Route is whitelisted
$matchedRoute = $match->getMatchedRouteName();
if (in_array($matchedRoute, array('login'))) {
return;
}
// if they're logged in, all is good
if ($auth->hasIdentity()) {
return true;
}
[render login form and return response object]
}, -100);
This works great.
The API also sometimes expires the login tokens in a way that I can't easily predict, which means all API calls will return a 'Session expired' type error. I've written an event trigger after the API calls that I can hook into. I want to check for these 'session expired' responses and somehow render the login page in the same way I do above:
$events->attach('Api', 'call', function ($e) {
$api = $e->getTarget();
$params = $e->getParams();
$result = $params['apiResult'];
if ([result is a session expired response]) {
// what can I do here?
}
}, 999);
but since this isn't an MVC event, even if I could access the response object here, returning it wouldn't do anything. What would be the best way to interrupt the application flow in a non-MVC event?

I'm not sure but I'm assuming that your API events do occur in a dedicated EventManager instance (so your API may be an implementation of EventManagerAwareInterface) and not in the MVC one (which is the one you grab from the Zend\Mvc\Application instance).
If that's the case, you could inject both the main EventManager and the MvcEvent inside your API, and then short circuit the MVC cycle from the call listener.
I.e. assume your dependencies are in $mvcEvent and $mvcEventManager properties with getters, this is how you would listen for the call event:
$events->attach('call', function($e) {
$api = $e->getTarget();
$params = $e->getParams();
$result = $params['apiResult'];
if ([result is a session expired response]) {
$mvcEvent = $api->getMvcEvent();
$mvcEvent->setError('api error');
$mvcEvent->setParam('exception', new \Exception('Session expired'));
$api->getMvcEventManager()->trigger('dispatch.error', $mvcEvent);
}
}, 999);
There are better ways to do it for sure, choosing the best will depend on the architecture of your API class.
You could use Zend\EventManager\ResponseCollection returned by your trigger, rather than using the MVC event inside the listener; that would enable your API event cycle to continue even if some error occurs. That's actually how Zend\Mvc\Application uses its own event manager in the run() method, so you can peek at that for an example.

Related

MSAL.NET redirect loop when using graphApi in MVC & blazor with multiple instances

I have created a blazor component that aims to simplify managing users and group of an enterprise application in my ASP.NET MVC website. When I run the code locally, everything works just fine. However, when I deploy my code on the dev environment (in AKS) the code only works if I run one replica.
When I use multiple instances and I try to access the page that calls my blazor component, the page ends up in a redirect loop, and finally shows the Microsoft login interface with an error mentioning that the login was not valid.
This is how my code looks like:
# program.cs
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
var cacheOptions = builder.Configuration.GetSection("AzureTableStorageCacheOptions").Get<AzureTableStorageCacheOptions>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24);
});
builder.Services.AddDistributedAzureTableStorageCache(options =>
{
options.ConnectionString = cacheOptions.ConnectionString;
options.TableName = cacheOptions.TableName;
options.PartitionKey = cacheOptions.PartitionKey;
options.CreateTableIfNotExists = true;
options.ExpiredItemsDeletionInterval = TimeSpan.FromHours(24);
});
builder.Services.AddSession();
...
# The controller that calls the blazor component
[AuthorizeForScopes(Scopes = new[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" })]
public async Task<IActionResult> UserManagement()
{
string[] scopes = new string[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" };
try
{
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex);
}
return View();
}
And this is what happens:
If the page loads, I can see this exception in the pod logs:
What am I doing wrong?
The tenant actually needs to provide admin consent to your web API for the scopes you want to use for replicas for the token taken from cache.
Also when AuthorizeForScopes attribute is specified with scopes ,this needs the exact scopes that is required by that api. MsalUiRequiredException gets thrown in case of incorrect scopes for that api and results in a challenge to user.
This error may also occur even when the acquiretokensilent call will not have a valid cookie anymore for authentication in cache .Please check how acquiretokensilent call works from here in msal net acquire token silently | microsoft docs
When valid scopes are given , please make sure the permissions are granted consent by the admin directly from portal or during user login authentication.
Also as a work around try to use use httpContextAccessor to access
token after authentication .
Reference: c# - Error : No account or login hint was passed to the AcquireTokenSilent call - Stack Overflow
So, the culprit was:
#my controller
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
Which we were using initially to reauthenticate the graph api component when we were using InMemoryCache.
There is no need to get the access token again when using DistributedTokenCache, and actually that was causing the token to get saved / invalidated in an infinite loop.
Also, in my blazor component, I had to do use the consent handler to force a login:
private async Task<ServicePrincipal> GetPrincipal(AzureAdConfiguration addConfiguration)
{
try
{
return await GraphClient.ServicePrincipals[addConfiguration.PrincipalId].Request()
.Select("id,appRoles, appId")
.GetAsync();
}
catch (Exception ex)
{
ConsentHandler.HandleException(ex);
throw;
}
}

Aspnet core cookie [Authorize] not redirecting on ajax calls

In an asp.net core 3.1 web app with cookie-based authorization I have created a custom validator which executes on the cookie authorization's OnValidatePrincipal event. The validator does a few things, one of those is check in the backend if the user has been blocked. If the user has been blocked, The CookieValidatePrincipalContext.RejectPrincipal() method is executed and the user is signed out using the CookieValidatePrincipalContext.HttpContext.SignOutAsyn(...) method, as per the MS docs.
Here is the relevant code for the validator:
public static async Task ValidateAsync(CookieValidatePrincipalContext cookieValidatePrincipalContext)
{
var userPrincipal = cookieValidatePrincipalContext.Principal;
var userService = cookieValidatePrincipalContext.GetUserService();
var databaseUser = await userService.GetUserBySidAsync(userPrincipal.GetSidAsByteArray());
if (IsUserInvalidOrBlocked(databaseUser))
{
await RejectUser(cookieValidatePrincipalContext);
return;
}
else if (IsUserPrincipalOutdated(userPrincipal, databaseUser))
{
var updatedUserPrincipal = await CreateUpdatedUserPrincipal(userPrincipal, userService);
cookieValidatePrincipalContext.ReplacePrincipal(updatedUserPrincipal);
cookieValidatePrincipalContext.ShouldRenew = true;
}
}
private static bool IsUserInvalidOrBlocked(User user)
=> user is null || user.IsBlocked;
private static async Task RejectUser(CookieValidatePrincipalContext context)
{
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
And here is the setup for the cookie-based authorization:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(co =>
{
co.LoginPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Login)}";
co.LogoutPath = #$"/{ControllerHelpers.GetControllerName<AuthenticationController>()}/{nameof(AuthenticationController.Logout)}";
co.ExpireTimeSpan = TimeSpan.FromDays(30);
co.Cookie.SameSite = SameSiteMode.Strict;
co.Cookie.Name = "GioBQADashboard";
co.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = UserPrincipalValidator.ValidateAsync
};
co.Validate();
});
This actually gets called and executed as expected and redirects the user to the login page when they navigate to a new page after having been blocked.
Most of the views have ajax calls to api methods that execute on a timer every 10 seconds. For those calls the credentials also get validated and the user gets signed out. However, after the user has been signed out, a popup asking for user credentials appears on the page:
If the user doesn't enter their credentials and navigate to another page, they get taken to the login page as expected.
If they do enter their credentials, they stay logged in, but their identity appears to be their windows identity...
What is going on here? What I would really want to achieve is for users to be taken to the login page for any request made after they have been signed out.
I have obviously misconfigured something, so that the cookie-based authorization doesn't work properly for ajax requests, but I cannot figure out what it is.
Or is it the Authorization attribute which does not work the way I'm expecting it to?
The code lines look good to me.
This login dialog seems to be the default one for Windows Authentication. Usually, it comes from the iisSettings within the launchSettings.json file. Within Visual Studio you'll find find the latter within your Project > Properties > launchSettings.json
There set the windowsAuthentication to false.
{
"iisSettings": {
"windowsAuthentication": false,
}
}

OWIN authentication middleware: logging off

OWIN beginner here. Please be patient...
I'm trying to build an OWIN authentication middleware which uses form posts to communicate with my external authentication provider. I've had some success with getting the authentication bits working. In other words, I can:
communicate with the remote provider through form post;
process the response returned by the remove provider
If everything is ok, I'm able to signal the default authentication provider
THis in turn gets picked up by the cookie middleware which ends up generating the authentication cookie
So far, so good. Now, what I'd like to know is how to handle a log off request. Currently, the controller will simply get the default authentication manager from the owin context and call its SingOut method. This does in fact end my current session (by removing the cookie), but it really does nothing to the existing "external" session.
So, here are my questions:
1. Is the authentication middleware also responsible for performing log off requests?
2. If that is the case, then can someone point me to some docs/examples of how it's done? I've found some links online which describe the logging in part, but haven't found anything about the log off process...
Thanks.
Luis
After some digging, I've managed to get everything working. I'll write a few tips that might help someone with similar problems in the future...
Regarding the first question, the answer is yes, you can delegate the logoff to the middleware. If you decide to do that, then your middleware handler should override the ApplyResponseGrantAsync method and check if there's a current revoke request. Here's some code that helps to illustrate the principle:
protected override async Task ApplyResponseGrantAsync() {
var revoke = Helper.LookupSignOut(Options.AuthenticationType,
Options.AuthenticationMode);
var shouldEndExternalSession = revoke != null;
if (!shouldEndExternalSession) {
return;
}
//more code here...
}
After checking if there's a revoke request, and if your external authentication provider is able to end the response through a redirect, then you can simply call the Response.Redirect method (don't forget to check for the existance of redirect - ex.: if you're using asp.net identity and MVC's automatically generated code, then the sign out will redirect you to the home page of your site).
In my scenario, things were a little more complicated because communication with my authentication provider was based of form posts (SAML2 messages with HTTP Post binding). I've started by trying to use Response.Write to inject the HTML with the autopostback form into the output buffer:
protected override async Task ApplyResponseGrantAsync() {
//previous code + setup removed
var htmlForm = BuildAndSubmitFormWithLogoutData(url,
Options.UrlInicioSessaoAutenticacaoGov);
Response.StatusCode = 200;
Response.ContentType = "text/html";
await Response.WriteAsync(htmlForm);
}
Unfortunately, it simply didn't work out. Not sure on why, but the browser insisted in redirecting the page to the URL defined by the Logoff's controller method (which was redirecting the page to its home page or '/'). I've even tried to remove the location HTTP header from within the ApplyResponseGrantAsync method, but it still ended up redirecting the user to the home page (instead of loading the predefined HTML I was writing).
I've ended up changing the redirect so that it gets handled by my middleware. Here's the final code I've ended up with in the ApplyResponseGrant method:
protected override async Task ApplyResponseGrantAsync() {
//previous code + setup removed
//setup urls for callbabk and correlation ids
var url = ...; //internal cb url that gets handled by this middleware
Response.Redirect(url);
}
This redirect forced me to change the InvokeAsync implementation so that it is now responsible for:
Checking for a new authentication session
Checking for the end of an existing authentication session (handle the logoff response from the external provider)
Checking if it should generate a new form post html message that ends the current session controlled by the external provider
Here's some pseudo code that tries to illustrate this:
public override async Task<bool> InvokeAsync() {
if (Options.InternalUrlForNewSession.HasValue &&
Options.InternalUrlForNewSession == Request.Path) {
return await HandleLoginReply(); /login response
}
if (Options.InternalUrlExternalSessionEnded.HasValue &&
Options.InternalUrlExternalSessionEnded == Request.Path) {
return await HandleLogoffReply();//logoff response
}
if (Options.InternalUrlForEndingSession.HasValue &&
Options.InternalUrlForEndingSession == Request.Path) {
return await HandleStartLogoutRequest(); //start logoff request
}
return false;
}
Yes, in the end, I've ended with an extra request, which IMO shouldn't be needed. Again, I might have missed something. If someone manages to get the ApplyResponseGrantAsync to return the auto submit post (instead of the redirect), please let me know how.
Thanks.

PRG BestPractice Zf2

i have an best practice question.
Its clear what Post/Redirect/Get does, but what will be the bestpractice to handle them?
I think there are 2 methods to handle them.
1.) We call the prg plugin at first on controller action
2.) We first validate the post data, and only redirect to the prg-response if successfull?
My problem about this is, at
1.) We enlarge the response time because of the redirect, this is by default so i think not the best solution
2.) will create an overhead by the every time validation of the form
What did you mean is the better solution aber this case?
regards
UPDATE:
What i mean is, the normal(standard) case is something like this - http://framework.zend.com/manual/2.0/en/modules/zend.mvc.plugins.html#the-post-redirect-get-plugin.
$prg = $this->prg('url');
if ($prg instanceof Response) {
return $prg;
} elseif ($prg === false) {
return new ViewModel(array(...));
}
$form->setData($prg);
This means, that theres after every form submit an redirect executes.
Now, my idea was something like this:
$prg = $this->prg();
$form = $this->getFormLogin();
$data = ($prg instanceof ResponseInterface)
? $this->getRequest()->getPost()
: $prg;
if (false !== $data) {
$form->setData($data);
if (true === $form->isValid()) {
if ($prg instanceOf ResponseInterface) {
return $prg;
}
// Make something within the loginservice or something else
}
The idea behind this was, to only redirect for the PRG only if the form is valid, to save response time and other things (because of bootstrapping settings etc.)
The Zend Framework is designed based on Front-Controller pattern so its essential to redirect the page when you access different resources(controller-action).
moreover when you fire redirect(URL) function from your source code it takes minimal time when you compared the time to access the same(URL) from your browser.
You could reduce the response time to considerable amount when you use classmap_autoloading.
Updated:
for an example i take login process, in the below code i implement both HTTP get and post methods in the same action() but, you can refactor this function based on HTTP methods.
LoginController.php
public function loginAction()
{
//code
if ($request->isPost()) {
//code
if ($isValid) {
return $this->redirect()->toUrl('urUrl');
}
return $this->redirect()->toUrl('urUrl');
}
//code
return $viewModel;
}
After refactoring above code
//it used for HTTP get
public function loginAction()
{
//code
return $viewModel;
}
//it used for HTTP POST
public function loginPostAction()
{
//code
if ($notValid) {
return $this->redirect()->toUrl('urUrl');
}
$viewModel->setTemplate($viewpath);
return $viewModel;
}
You need to modify your routing configuration in such a way to handle for both HTTP get and post methods. if the request is HTTP-get the controller process the loginAction() but if its HTTP-post it process the loginPostAction()
Zend framework 2 - HTTP method Routing
Updated:
The purpose of plugin is to avoid the user to POST the data again to the browser. In your case you are trying to enable the option to POST their data when the form is not valid (you are trying to change the behaviour of PRG plugin). if you really worried about response time don't use PRG plugin. create your custom logic inside your controller-action.
--SJ

Logging specific request header using spring security events

In my grails application, failed login attemps get logged using spring security events as shown here http://grails-plugins.github.com/grails-spring-security-core/docs/manual/guide/single.html#7.3%20Registering%20Callback%20Closures
My issue has to do with client ip retrieval. Normally, calling getRemoteAddress from details object of the event should do the job, but my case is that my application is behind a reverse proxy therefore i should get the ip from request header X-Forwarded-For.
Neither event object nor application context parameters of the closuse provide access to the request object. The global request object isn't available either.
Any ideas how to get access to headers or any other way to implement this functionality?
You can get it from RequestContextHolder, if it exists:
GrailsWebRequest request = RequestContextHolder.currentRequestAttributes()
request.getHeader("X-Forwarded-For")
Generally, as you probably know, it isn't considered a very good idea to access the web session from within Services. First of all, you break the abstraction and separation of service logic, and requests might not always be available or associated with the current thread. One way to access the session from a service is to encapsulate the HTTP session in the following manner:
class WebUtilService {
void withSession (Closure closure) {
try {
GrailsWebRequest request = RequestContextHolder.currentRequestAttributes()
GrailsHttpSession session = request.session
closure.call(session)
}
catch (IllegalStateException ise) {
log.warn ("No WebRequest available!")
}
}
}
and you would use it like this:
class MyService {
WebUtilService webUtilService
void doSomething() {
webUtilService.withSession { HttpSession session ->
log.info(session.myValue)
session.newValue = 'Possible, but should be exceptional'
}
}
}
where you could have access to the getHeader() method.
Disclaimer: the code is from Marc-Oliver Scheele's blog.

Resources