AngularJS hash url and MVC routing - angularjs

I am facing issue in navigating to URL. My default page set to Login and my application URL is http://localhost:12345/#/.
This works well but there are two ways the user can login to application
Direct through the application.
Getting username and password trough query string.
When application is logging through Query String the url is like http://localhost:12345?auth=123654654656564/#/.
I would like to remove auth value from the URL. I tried to map the routing but it doesn't work.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}",
defaults: new { controller = "Account", action = "Login"}
);
And also i tried to create one more action result that will return the view
routes.MapRoute(
name: "ActualDefault",
url: "{controller}/{action}",
defaults: new { controller = "Account", action = "LoginQuery" }
);
Controller:
public ActionResult Login()
{
if (Request.QueryString.Count > 0 && Request.QueryString != null)
{
//validating
return RedirectToAction("LoginQuery", "Account");
}
else
{
return View();
}
}
public ActionResult LoginQuery()
{
return View("Index");
}
The above code removes query string but the URL will be http://localhost:12345/Account/LoginQuery/#/.
I just need the URL like http://localhost:12345/#/.

Logging in via Query String
I would be negligent not to point out that this is an extremely bad practice. You should always use HTTP POST when logging into an application and send the user secrets in the body of the post, not the query string.
See
Handling Form Edit and Post Scenarios
Submit Form with Parameters in ASP.NET MVC
BUILDING ASP.NET MVC FORMS WITH RAZOR
Note that you can also create forms in plain HTML (or via angularjs) to call an MVC action method, or you can make an HTTP POST via JavaScript or some other programming language to do the same thing.
Query string values are completely ignored by MVC routing. But you can make a custom route use query string values.
public class LoginViaQueryStringRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var path = httpContext.Request.Path;
if (!string.IsNullOrEmpty(path))
{
// Don't handle URLs that have a path /controller/action
return null;
}
var queryString = httpContext.Request.QueryString;
if (!queryString.HasKeys())
{
// Don't handle the route if there is no query string.
return null;
}
if (!queryString.AllKeys.Contains("username") && !queryString.AllKeys.Contains("password"))
{
// Don't handle the case where controller and action are missing.
return null;
}
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["controller"] = "Account";
routeData.Values["action"] = "LoginQuery";
routeData.Values["username"] = queryString["username"];
routeData.Values["password"] = queryString["password"];
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new LoginViaQueryStringRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
This route will now match http://localhost:12345/?username=foo&password=bar and send it to your LoginQuery action method.
Logging in via http://localhost:12345/#/
It is unclear how you expect this to work. Since everything after the hash tag are generally not sent to the server from the browser, http://localhost:12345/#/ is equivalent to http://localhost:12345/. So, you are effectively saying "I want my home page to be the login page".
In a typical MVC application, you would setup an AuthorizeAttribute on the home page to redirect the user to the login page. After the user logs in, they would be redirected back to the home page (or usually whatever secured page they initially requested).
[Authorize]
public ActionResult Index()
{
return View();
}
If you want all of your application secured, you can register the AuthorizeAttribute globally and use AllowAnonymousAttribute on your public action methods (such as the login and register pages).
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AuthorizeAttribute());
filters.Add(new HandleErrorAttribute());
}
}
And your login action methods:
[AllowAnonymous]
public ActionResult Login()
{
//...
}
[AllowAnonymous]
[HttpPost]
public ActionResult Login(LoginModel model)
{
//...
}
[AllowAnonymous]
public ActionResult LoginQuery(string username, string password)
{
//...
}
But then, that is a typical MVC-only application.
If you are using Angular to make a SPA, then this could be a very different story. Namely, you would probably switch views on the client side without doing an HTTP 302 redirect to the login form (perhaps it would be a popup - who knows). The point is, without any details of how the client is setup to communicate with MVC, it is not possible to give you any useful advice on setting up MVC for your client beyond how you would typically setup MVC to work in a multi-page application.
NOTE: I can tell you that your routing is misconfigured. The Default and ActualDefault definitions cannot exist in the same route configuration because the first match always wins, therefore the first one will run and the other one will never run. Both of the route URL definitions will match any URL that is 0, 1, or 2 segments in length, so whichever you have first in the route table will match and the other one will be an unreachable execution path.

Related

Blazor Wasm setting end user defined startpage

In my application I have many areas for different kinds of users. Controlled by roles.
So I would like to give the user an option to set a preferred startpage.
Deep Linking works, so no problems there.
My first attempt was this
#code{
[Parameter] public string Action { get; set; }
[Inject] private NavigationManager Navigation { get; set; }
private void LoginSucceeded(RemoteAuthenticationState state)
{
Console.WriteLine("navigate to DepartmentAccess");
// This works with on extra login
Navigation.NavigateTo("/DepartmentAccess", true);
// This loads 3 or more times
// state.ReturnUrl = "/DepartmentAccess";
}
}
Then I tried altering the return url in RedirectToLogin.razor. Here I added “MyStartPage”
#inject NavigationManager Navigation
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#using Zeus.Client.PortfolioRights
#code {
protected override void OnInitialized()
{
Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)/MyStartPage}");
}
}
This has absolutely no effekt!
Okay time to dig a little deeper
It’s the RemoteAuthenticatorViewCore that holds all the code related to the login process.
This function handles the Login state. It’s called after the redirect from Azure AD
private async Task ProcessLogIn(string returnUrl)
{
AuthenticationState.ReturnUrl = returnUrl;
var result = await AuthenticationService.SignInAsync(new RemoteAuthenticationContext<TAuthenticationState>
{
State = AuthenticationState
});
switch (result.Status)
{
case RemoteAuthenticationStatus.Redirect:
break;
case RemoteAuthenticationStatus.Success:
await OnLogInSucceeded.InvokeAsync(result.State);
await NavigateToReturnUrl(GetReturnUrl(result.State, returnUrl));
break;
case RemoteAuthenticationStatus.Failure:
_message = result.ErrorMessage;
Navigation.NavigateTo(ApplicationPaths.LogInFailedPath);
break;
case RemoteAuthenticationStatus.OperationCompleted:
default:
throw new InvalidOperationException($"Invalid authentication result status '{result.Status}'.");
}
}
The input parameter “returnUri” are set from this function
private string GetReturnUrl(TAuthenticationState state, string defaultReturnUrl = null)
{
if (state?.ReturnUrl != null)
{
return state.ReturnUrl;
}
var fromQuery = QueryStringHelper.GetParameter(new Uri(Navigation.Uri).Query, "returnUrl");
if (!string.IsNullOrWhiteSpace(fromQuery) && !fromQuery.StartsWith(Navigation.BaseUri))
{
// This is an extra check to prevent open redirects.
throw new InvalidOperationException("Invalid return url. The return url needs to have the same origin as the current page.");
}
return fromQuery ?? defaultReturnUrl ?? Navigation.BaseUri;
}
So I wonder. Why is the “defaultReturnUrl” not set when I alter the return uri?
Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)/MyStartPage}
I guess that I don't understand the login flow. But got a feeling that I am close.
Just need to find a way to set the defaultReturnUrl
Please check if below points can be worked around.
The first step in getting a page (component) ready to participate in routing is to assign the component a route. You do that in the cshmtl file, using the page directive.
#page "/redirectpage" above #inject NavigationManager NavManager
Make sure to Add CascadingAuthenticationState and <AuthorizeView> in apps.razor and LoginDisplay component.It is responsible to display pages that the user is authorised to see.
(or)
If the page component for the route contains an authorize attribute (#attribute [Authorize]) then the user must be logged in, otherwise they will be redirected to the login page.
Example: if you wanted to secure the Counter.razor page just add an Authorize attribute to the top:
#using Microsoft.AspNetCore.Authorization
#attribute [Authorize]
Or
#attribute [Authorize(Roles = "finance")]
Add this to the pages that require authentication.
References:
Secure an ASP.NET Core Blazor WebAssembly standalone app with the
Authentication library | Microsoft Docs
Using Azure Active Directory to Secure Blazor WebAssembly Hosted
Apps (code-maze.com)
Secure an ASP.NET Core Blazor WebAssembly standalone app with Azure
Active Directory | Microsoft Docs

.NET Core 3.1 web application with React - how to prevent access based on Active Directory group

I have a .NET Core 3.1 web application with React using windows authentication.
When a user enters their Active Directory credentials i would like to verify they belong to a particular Active Directory group before allowing access to the React app.
I have tried setting the default endpoint to a Login Controller to verify the user's groups but i don't know how to redirect to the React app if they do have the valid group.
Startup.cs:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}",
defaults: new { Controller = "Login", action = "Index" });
});
LoginController:
public IActionResult Index()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
string[] domainAndUserName = HttpContext.User.Identity.Name.Split('\\');
//AuthenticateUser verifies if the user is in the correct Active Directory group
if (AuthenticateUser(domainAndUserName[0], domainAndUserName[1]))
{
//This is where i would like to redirect to the React app
return Ok(); //This does not go to the react app
return LocalRedirect("http://localhost:50296/"); //This will keep coming back to this method
}
return BadRequest();
}
}
Is it possible to redirect to the React app from the controller?
Is there a better way to verify an active directory group, possibly through authorizationService.js?
I've been in this situation before, and solved it with custom implementation of IClaimsTransformation. This approach may also be used with OpenId Connect and other authentication systems that requires additional authorization.
With this approach, you can use authorize attribute on controller that serves your React app
[Authorize(Roles = "HasAccessToThisApp")]
and
User.IsInRole("HasAccessToThisApp")
elsewhere in code.
Implementation. Please note that TransformAsync will be called on every request, some caching is recommended if any time-consuming calls.
public class YourClaimsTransformer : IClaimsTransformation
{
private readonly IMemoryCache _cache;
public YourClaimsTransformer(IMemoryCache cache)
{
_cache = cache;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal incomingPrincipal)
{
if (!incomingPrincipal.Identity.IsAuthenticated)
{
return Task.FromResult(incomingPrincipal);
}
var principal = new ClaimsPrincipal();
if (!string.IsNullOrEmpty(incomingPrincipal.Identity.Name)
&& _cache.TryGetValue(incomingPrincipal.Identity.Name, out ClaimsIdentity claimsIdentity))
{
principal.AddIdentity(claimsIdentity);
return Task.FromResult(principal);
}
// verifies that the user is in the correct Active Directory group
var domainAndUserName = incomingPrincipal.Identity.Name?.Split('\\');
if (!(domainAndUserName?.Length > 1 && AuthenticateUser(domainAndUserName[0], domainAndUserName[1])))
{
return Task.FromResult(incomingPrincipal);
}
var newClaimsIdentity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Role, "HasAccessToThisApp", ClaimValueTypes.String)
// copy other claims from incoming if required
}, "Windows");
_cache.Set(incomingPrincipal.Identity.Name, newClaimsIdentity,
DateTime.Now.AddHours(1));
principal.AddIdentity(newClaimsIdentity);
return Task.FromResult(principal);
}
}
In Startup#ConfigureServices
services.AddSingleton<IClaimsTransformation, YourClaimsTransformer>();

ASP.NET MVC | ActionResult not getting called when going back to previous page

I understand that the title of the question may be vague but then that's the best way I could come up with to explain my issue at hand.
I'm overriding the OnActionExecuting function to manage my session related activities and allow/ deny requests to authorized/ unauthorized users, respectively. Along with tracking of the session, I'm also using the OnActionExecuting to load user available features for the current page into a temporary class and accessing from the view using ajax call.
namespace MyApp.Controllers
{
public class TESTController : Controller
{
[SessionTimeout]
public ActionResult Index()
{
return this.View();
}
}
}
public class SessionTimeoutAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
HttpContext ctx = HttpContext.Current;
if (ctx.Session["AppUser"] == null)
{
// Redirect to the login page
// Or deny request
}
else
{
var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
var actionName = filterContext.ActionDescriptor.ActionName;
var methodType = ((ReflectedActionDescriptor)filterContext.ActionDescriptor).MethodInfo.ReturnType;
if (methodType == typeof(ActionResult))
{
// Load all user access rights for the current page into a temporary memory
// by using the Action and Controller name
}
}
base.OnActionExecuting(filterContext);
}
}
The above works like a charm.. But the issue is when the user clicks on the back button of the browser or hits the backspace key. In that case, the OnActionExecuting function is never called for the ActionResult and further I am unable to load the current page access rights for the user.
Thanks & Regards,
Kshitij
Adding the following to my ActionResult made the above code to work.
[SessionTimeout]
[OutputCache(Duration = 0, NoStore = true)]
public ActionResult SomeView()
{
return this.View();
}

Why is this Web API 2 action returning 404 not found?

Here is my ApiController:
[RoutePrefix("api/Campaign")]
public class CampaignController : ApiController
{
private readonly ICampaignLogic _campaignLogic;
public CampaignController(ICampaignLogic campaignLogic)
{
_campaignLogic = campaignLogic;
}
[HttpGet]
[Route("GetCampaignsByYearCompanyAndContactIds/{companyId:int}/{contactId:int}/{year:int}")]
public List<Campaign> GetCampaignsByYearCompanyAndContactIds(int companyId, int contactId, int year)
{
return _campaignLogic.GetByYearCompanyAndContactIds(year, companyId, contactId);
}
}
And here is where I make the call in AngularJS:
$http.get(ApiUrl + "/api/Campaign/GetCampaignsByYearCompanyAndContactIds",
{ params: { year: $scope.year, companyId: $scope.client, contactId: $scope.contactId } })
.then(function (responses) {
})
This is what is being called when I debug it in Chrome:
http://localhost/MyApi/api/Campaign/GetCampaignsByYearCompanyAndContactIds?companyId=12046&contactId=13662&year=2016
Everything is set up correctly in terms of my localhost as I am calling other API actions without any problem. However, when I call this one I am getting a 404 Not Found error.
What am I missing here?
Based on your route the url needs to have the parameters included in the route and not a part of the query string. if you want to make them available only in the query string then remove them from the route attribute.
What you should be passing in AngularJS
http://localhost/MyApi/api/Campaign/GetCampaignsByYearCompanyAndContactIds/12046/13662/2016
So you will need to build the URL using the parameters, you cannot use params with this url.
If you want to use params in AngularJS change the Route attribute to the following.
[Route("GetCampaignsByYearCompanyAndContactIds")]

ServiceStack Self Hosting Redirect any 404 for SPA (Url Rewrite)

I'm using self hosting servicestack for a single page application with mvc and razor. On the same apphost i serve some api. The problem is i need some a 404 redirect to the main path "/" to get html5 mode running in AngularJS. I've tried:
DefaultRedirectPath = "/"
this.GlobalHtmlErrorHttpHandler = new RazorHandler("/");
How can i get any 404 request to the main page?
You can register a Fallback Route with a wildcard for this which will let you handle unknown server routes in your Service in order to return the default page so routing can be handled in the client app, e.g:
[FallbackRoute("/{PathInfo*}")]
public class FallbackForClientRoutes
{
public string PathInfo { get; set; }
}
public class MyServices : Service
{
public object Any(FallbackForClientRoutes request)
{
return new HttpResult {
View = "/default.cshtml"
};
}
}
This is also the strategy used by the techstacks.io AngularJS Single Page App which also supports server-generated pages with techstacks.io/?html=server.

Resources