How do you setup special characters for Identity Core LoginPath? - asp.net

I'm using ASP.NET Identity Core for authentication. I have an AngularJS SPA with a login route (UI-Router). My user's are getting routed to an escaped URL instead of the real URL.
My login path is set to "/#!/login", but my users are being routed to: "/%23!/login" which causes a 401 in the browser.
I've tried using System.Uri.EscapeDataString, System.Uri.EscapeUriString and without escaping at all with no luck.
Startup.cs
services.AddAuthentication()
.AddCookie(cookie =>
{
cookie.LoginPath = System.Uri.EscapeDataString("/#!/login");
})
AngularJS Route
.state('login', {
url: 'login',
views: {
'': { templateUrl: './Home/login.html', controller: "loginController" }
}
})
I've confirmed the server is generating a 302 response with the following location: "http://localhost:63939/%23!/login?ReturnUrl=%2Fadministrator". So the server is definitely escaping the "#!" and it is not something the browser is doing.

The only thing I've been able to figure out is to unescape the RedirectUri in the OnRedirectToLogin cookie authentication event. There has got to be a better way.
cookie.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = context =>
{
context.Response.Redirect(System.Uri.UnescapeDataString(context.RedirectUri));
return Task.FromResult(0);
}
};

Related

Next.js does not render authentication state properly after keycloak login

I have a basic next.js application that does two things:
provide an authentication mechanism using keycloak
talk to a backend server that authorizes each request using the keycloak-access-token
I use the #react-keycloak/ssr library to achieve this. The problem now is that after I login and get redirected back to my application the cookie that contains the kcToken is empty. After I refresh my page it works like expected.
I understand that maybe my entire process flow is wrong. If so, what is the "usual" way to achieve what is mentioned above?
export async function getServerSideProps(context) {
const base64KcToken = context.req.cookies.kcToken // the cookie that keycloak places after login
const kcToken = base64KcToken ? Buffer.from(base64KcToken, "base64") : ""
// the backend server passes the token along to keycloak for role-based authorization
const res = await fetch(`${BACKEND_URL}/info`, {
headers: {
"Authorization": "Bearer " + kcToken
}
})
const data = await res.json()
// ... exception handling is left out for readability ...
return {
props: {
data
}
}
}
export default function Home({data}) {
const router = useRouter() // the next.js client side router to redirect to keycloak
const { keycloak, initialized } = useKeycloak() // keycloak instance configured in _app.js
if (keycloak && !initialized && keycloak.createLoginUrl) router.push(keycloak.createLoginUrl())
return (
<div> ... some jsx that displays data ... </div>
)
}
This process basically works but it feels really bad because a user that gets redirected after login is not able to see the fetched data unless he refreshes the entire page. This is because when getServerSideProps() is called right after redirect the base64KcToken is not there yet.
Also everything related to the login-status (eg. logout button) only gets displayed after ~1sec, when the cookie is loaded by the react-keycloak library.

Angular HttpClient calls are missing query string and Authorization header

When Angular makes a GET call using HttpClient, the query parameters and Authorization header are missing on the request in our QA environment. When running Angular locally, pointed to the QA APIs, it sends them both as expected.
Here's how the query parameters are set:
const params = new HttpParams().set('schedulingOnly', schedulingOnly ? 'true' : 'false');
return this.httpClient.get<any>(this.getBaseUrl() + '/domain/getAll', { params });
Here's how the Authorization header is set (interceptor):
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (environment.useHttpMockRequestInterceptor) {
return this.useMockData(request);
} else {
request = this.AddAuthenticationHeader(request);
return next.handle(request);
}
}
private AddAuthenticationHeader(request: HttpRequest<any>) {
const request = request.clone({
headers: request.headers
.set('Authorization', 'Bearer ' + sessionStorage.getItem('access_token'))
});
return request;
}
Here's what Chrome dev tools is showing:
That's all the basic information, but below is additional information about things I've tried without success.
Is this a CORS issue? - While searching for others with this issue, I came across a lot of CORS issues. I do not believe that's the case here because Angular and the APIs are on the same domain and I can run Angular locally and hit the APIs no problem.
Do query params get sent if I hardcode them into the url? - Yes. The following worked for the query params: return this.httpClient.get(this.getBaseUrl() + '/domain/getAll?schedulingOnly=true');
Is this something wrong with the interceptor? - I don't believe so. Console.log() statements show all the expected points in code being hit. In fact, the request object after the interceptor adds the auth header shows it on there.
I also tried setting directly without the interceptor, but no luck.
const obj = {
headers: { 'Authorization': 'Bearer ' + sessionStorage.getItem('access_token') },
params: { 'schedulingOnly': schedulingOnly ? 'true' : 'false' }
};
return this.httpClient.get<any>(this.getBaseUrl() + '/domain/getAll', obj);
There are no js errors in the console except the 401 error
QA web server is IIS
APIs are ASP.NET Core
Angular is embedded within an ASP.NET Web Forms project (due to migrating that legacy code into Angular incrementally)
The issue was that PrototypeJs was interfering with Angular. This led to the issue, but no warnings or errors, so it was just silently causing this issue. PrototypeJs is used in the containing ASP.NET Web Forms app that Angular is embedded into. The reason this was working locally, but not in QA is because I actually did have functionality to not load PrototypeJs if it was an Angular page, due to noticing other issues before, but that wasn't working in QA due to the site starting on a subpath, not directly on the host, so that functionality of not loading PrototypeJs wasn't working.
Have you tried with the shorter version of adding header in your interceptor:
const request = request.clone({
setHeaders: { 'Authorization': 'Bearer ' + sessionStorage.getItem('access_token') }
});
Interceptor
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (environment.useHttpMockRequestInterceptor) {
return this.useMockData(request);
} else {
request = this.AddAuthenticationHeader(request);
return next.handle(request);
}
}
private AddAuthenticationHeader(request: HttpRequest<any>) {
return request.clone({
setHeaders: {
Authorization: `Bearer ${sessionStorage.getItem('access_token')}`
}
});
return request;
}

AzureAD signout error mesage: "Hmm... we're having trouble signing you out."

I created a web application using ASP.NET CORE, using the quickstart guide in the AzureAD portal. Signing in works, but when I attempt to log out, after about 5 seconds, I get the following message:
https://i.imgur.com/RhOGaf6.png
My app has been registered with the following redirect URIs:
https://i.imgur.com/CAnQpM8.png
With https://localhost:52410/signout-oidc as logout URL, and implicit grand for ID tokens enabled.
I can see in the browser debug menu under network that there is no response
from the logout URL. So, I'm assuming that the error message pop ups because the logout URL takes too long to respond.
Note: If I reload the browser page with the error I do logout.
So I'm wondering how can I resolve this error message?
I solved this error by adjusting the launchSettings.json file.
I adjusted the iisExpress setting in iisSettings to use SSL like so:
"iisExpress": {
"applicationUrl": "http://localhost:3110/",
"sslPort": 44321
}
On top of this, I adjusted the port of my own application to also use 3110.
It seems that the configurations are fine. Here is an working sample for your reference. You can check the code for signing out.
[\[HttpGet\]
public IActionResult SignOut()
{
var callbackUrl = Url.Action(nameof(SignedOut), "Account", values: null, protocol: Request.Scheme);
return SignOut(
new AuthenticationProperties { RedirectUri = callbackUrl },
CookieAuthenticationDefaults.AuthenticationScheme,
OpenIdConnectDefaults.AuthenticationScheme);
}
\[HttpGet\]
public IActionResult SignedOut()
{
if (User.Identity.IsAuthenticated)
{
// Redirect to home page if the user is authenticated.
return RedirectToAction(nameof(HomeController.Index), "Home");
}
return View();
}][1]

ASP.NET 5 + Angular 2 routing (template page not REloading)

Angular 2 beta uses html5 routing by default.
However, when you go to a component and the route changes (eg http://localhost:5000/aboutus) and you reload/refresh the page, nothing is loaded.
The issue has been raised in this post also.
Most of the answers say that if we are going to pursue HTML5 routing in angular 2, then this issue of routing should be taken care of in server-side. More discussion here.
I am not sure how to handle this issue using the asp.net server environment.
Any angular 2 devs out there who also uses asp.net and encounters this issue?
PS. I'm using ASP.NET 5. My Angular 2 routes are using MVC routes.
The problem you're seeing has to do with the difference between Angular routing on the client and MVC server-side routing. You are actually getting a 404 Page Not Found error because the server does not have a Controller and Action for that route. I suspect you are not handling errors which is why it appears as if nothing happens.
When you reload http://localhost:5000/aboutus or if you were to try to link to that URL directly from a shortcut or by typing it into the address bar (deep linking), it sends a request to the server. ASP.NET MVC will try to resolve that route and in your case it will try to load the aboutusController and run the Index action. Of course, that's not what you want, because your aboutus route is an Angular component.
What you should do is create a way for the ASP.NET MVC router to pass URLs that should be resolved by Angular back to the client.
In your Startup.cs file, in the Configure() method, add an "spa-fallback" route to the existing routes:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// when the user types in a link handled by client side routing to the address bar
// or refreshes the page, that triggers the server routing. The server should pass
// that onto the client, so Angular can handle the route
routes.MapRoute(
name: "spa-fallback",
template: "{*url}",
defaults: new { controller = "Home", action = "Index" }
);
});
By creating a catch-all route that points to the Controller and View that ultimately loads your Angular app, this will allow URLs that the server does not handle to be passed onto the client for proper routing.
In your Startup.cs add this to the Configure method. This must be before other app statements.
app.Use(async (context, next) => {
await next();
if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value)) {
context.Request.Path = "/index.html"; // Put your Angular root page here
await next();
}
});
My favorite solution is to add the following code to Global.asax.cs which very smoothly and reliably takes care of the issue:
private const string RootUrl = "~/Home/Index";
// You can replace "~Home/Index" with whatever holds your app selector (<my-app></my-app>)
// such as RootUrl="index.html" or any controller action or browsable route
protected void Application_BeginRequest(Object sender, EventArgs e)
{
// Gets incoming request path
var path = Request.Url.AbsolutePath;
// To allow access to api via url during testing (if you're using api controllers) - you may want to remove this in production unless you wish to grant direct access to api calls from client...
var isApi = path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase);
// To allow access to my .net MVCController for login
var isAccount = path.StartsWith("/account", StringComparison.InvariantCultureIgnoreCase);
if (isApi || isAccount)
{
return;
}
// Redirects to the RootUrl you specified above if the server can't find anything else
if (!System.IO.File.Exists(Context.Server.MapPath(path)))
Context.RewritePath(RootUrl);
}
You need use this routing in ASP.NET MVC
app.UseMvc(routes =>
{
routes.MapRoute("Default", "{*url}", new { #controller = "App", #action = "Index" });
});
Then you need set up SystemJS with basePath options
The feature you're looking for is URL rewrite. There are two possible ways to handle it. The classic way is to let IIS do the work, as described here:
https://stackoverflow.com/a/25955654/3207433
If you don't want to depend on IIS, you can instead handle this in the ASP.NET 5 middleware, as shown in my answer here:
https://stackoverflow.com/a/34882405/3207433
I'm not having any luck getting
routes.MapRoute("Default", "{*url}",
new { #controller = "App", #action = "RedirectIndex" });
to work. I still get a 404 with any client side route.
Update:
Figured out why the catch-all route wasn't working: I had an attribute route defined ([Route("api/RedirectIndex")]) and while the plain route can be directly accessed with the fallback route it didn't fire. Removing the attribute route made it work.
Another solution that seems to work just as easy as the catch-all route handler is to just create a custom handler that fires at the end of the middleware pipeline in Configure():
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
//handle client side routes
app.Run( async (context) =>
{
context.Response.ContentType = "text/html";
await context.Response.SendFileAsync(Path.Combine(env.WebRootPath,"index.html"));
});
This basically ends up being the catch-all route that simply sends index.html out over the existing URL request if there was no other handler that picked up the request.
This works nicely even in combination with IIS Rewrite rules (in which case the above just won't ever get fired.
Wrote up a blog post on this topic:
Handling HTML5 Client Route Fallbacks in ASP.NET Core
Here are two more options for solving this problem. You can either add the hash location strategy to your app module.
import { LocationStrategy, HashLocationStrategy } from '#angular/common';
#NgModule({
imports: [.... ],
declarations: [...],
bootstrap: [AppComponent],
providers: [
{
provide: LocationStrategy,
useClass: HashLocationStrategy
}
]
})
export class AppModule { }
This option will only work for the parts of your Angular2 app that live on the Home ASP Controller
Your second option is to add routes to your ASP Controller that match your Angular 2 app routes and return the "Index" View
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[ActionName("Angular-Route1")]
public IActionResult AngularRoute1()
{
return View("Index");
}
public IActionResult Route2()
{
return View("Index");
}
}
Did you use:
directives: [RouterOutlet, RouterLink] in the component.
apply the #ZOXEXIVO's solution then, in your _Layout.cshtml add this:
<head>
<base href="/"/>
.....
</had>
You can use both the routing
when you call Home/Index from angular routing.
write
Home/Index.cshtml
<my-app></my-app>
app.routing.ts
const routes: Routes = [
{ path: '', redirectTo: '/Home/Index', pathMatch: 'full' },
{ path: 'Home/Index', component: DashboardComponent }
]
So When URL will be Home/Index
will load the component of active url so it will load dashboard component.
The above selected solution did not work for me I also got 404 after following all the comments to the T. I am using an angular5 app in an MVC5 app. I use the default index landing page as the start for the angular5. My angular app is in a folder named mvcroot/ClientApp/ but on ng build it puts the distributed files in mvcroot/Dist/ by altering one setting in the .angular-cli.json file with "outDir": "../Dist"
This solution did work though.
This way only routes in the Dist directory get the fall over. Now you can hit refresh every time and exact route for the angular5 app reloads while staying on the correct component. Be sure to put the catch all first. On a side note, if using a token auth in your angular5, save the token to window.localStorage (or some other mechanism outside your angular5 app) as hitting refresh will wipe out all memory where you you maybe storing your token in a global variable. This keeps the user from having to login again if they refresh.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Catch All",
"dist/{*url}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Thinktecture IdentityServer v3 with WebForms and WebApi

I am trying for a while to figure out how to solve SSO (Single Sign On) with Thinktecture IdentityServer v3 for a legacy webforms application. Unfortunately I am stacked.
The infrastructure is like this:
A WebForm App which need authentication and Authorization (possibly
cookie or bearer token)
A javascript lightweight app (once the user is authenticated) makes requests to an WebApi (which is on separate domain)
I am having the following questions which hopefully will help me to bring things up:
I can't make the legacy webforms application to redirect to IdentityServer, even with set in the Web.Config. I have in the Startup.cs the app.UseCookieAuthentication(....) and app.UseOpenIdConnectAuthentication(....) correctly set ( I guess ). For MVC the [Authorize] attribute force the redirection to the IdentityServer. How this should be done for webforms?
Is there a way once the user is logged in, to reuse the token stored in the cookie as bearer token to the WebApi calls, made from the javascript client. I just want to do the requests to the WebApi on behalf on currently logged user (once again the webforms app and the webapi are on different domains)
Any help will be much appreciated.
Thanks!
I'm currently working on the same type of project. This is what I have found out so far.
There is 4 Separate Concerns.
Identity Server - Maintains Authenticating Users / Clients / Scope
WebApi - Consumes Token generated by Identity Server for Authorization & Identity Information of User.
WebForms / JQuery - For my project currently handles authentication for existing functionality redirects to the new WebApi.
HTML using Javascript - Strictly uses WebApi for Information.
The custom grant below is for a user currently logged in through the WebForm as a membership object & I do not want to ask the user again to relogin via Identity Server.
For direct oAuth Authentication check out the sample here..
Sample Javascript Client
Configuring the Javascript an Implicit Flow would work just fine. Save the token connect with the api.
Identity Server v3
I had to configured using
Custom Grant w IUserService
Custom Grants
These will show how to configure a custom grant validation. With the user service you can have the Identity Service query existing users & customize claims.
There is alot of configuration to the Identity Server to make it your own. this is al very well documented on the IdentityServer website I wont go in how to set the basics up.
Ex: Client Configuration
return new List<Client>
{
new Client
{
ClientName = "Custom Grant Client",
Enabled = true,
ClientId = "client",
ClientSecrets = new List<ClientSecret>
{
new ClientSecret("secret".Sha256()),
},
Flow = Flows.Custom,
CustomGrantTypeRestrictions = new List<string>
{
"custom"
}
}
};
WebApi - Resource
Example
WebApi Client Sample
Need to have the Nuget package
Thinktecture.IdentityServer.AccessTokenValidation
Startup.cs
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
//Location of your identity server
Authority = "https://localhost:44333/core"
});
WebForms
BackEnd WebForms Call
Need Nuget Package
Thinktecture.IdentityModel.Client
[WebMethod]
[ScriptMethod(ResponseFormat.Json)]
public static string AuthorizeClient()
{
var client = new OAuth2Client(
//location of identity server, ClientId, ClientSecret
new Uri("http://localhost:44333/core/connect/token"),
"client",
"secret");
//ClientGrantRestriction, Scope (I have a Client Scope of read), Listing of claims
var result = client.RequestCustomGrantAsync("custom", "read", new Dictionary<string, string>
{
{ "account_store", "foo" },
{ "legacy_id", "bob" },
{ "legacy_secret", "bob" }
}).Result;
return result.AccessToken;
}
These are generic claim for this example however I can generate my own claim objects relating to the user to send to the Identity Server & regenerate an Identity for the WebApi to consume.
WebForms / JQuery
using
JQuery.cookie
$('#btnTokenCreate').click(function (e) {
//Create Token from User Information
Ajax({
url: "Default.aspx/AuthorizeClient",
type: "POST"
},
null,
function (data) {
sendToken = data.d;
//Clear Cookie
$.removeCookie('UserAccessToken', { path: '/' });
//Make API Wrap Info in Stringify
$.cookie.json = true;
//Save Token as Cookie
$.cookie('UserAccessToken', sendToken, { expires: 7, path: '/' });
});
JQuery WebAPI Ajax
Sample Ajax Method - Note the beforeSend.
function Ajax(options, apiToken, successCallback) {
//Perform Ajax Call
$.ajax({
url: options.url,
data: options.params,
dataType: "json",
type: options.type,
async: false,
contentType: "application/json; charset=utf-8",
dataFilter: function (data) { return data; },
//Before Sending Ajax Perform Cursor Switch
beforeSend: function (xhr) {
//Adds ApiToken to Ajax Header
if (apiToken) {
xhr.withCredentials = true;
xhr.setRequestHeader("Authorization", " Bearer " + apiToken);
}
},
// Sync Results
success: function (data, textStatus, jqXHR) {
successCallback(data, textStatus, jqXHR);
},
//Sync Fail Call back
error: function (jqXHR, textStatus, errorThrown) {
console.log(errorThrown);
}
});
}
AngularJS
This has same idea as the JQuery using the
module.run(function($http) {
//Make API Wrap Info in Stringify
$.cookie.json = true;
//Save Token as Cookie
var token = $.cookie('UserAccessToken');
$http.defaults.headers.common.Authorization = 'Bearer ' + token });
This makes the assumption your using the same domain as the WebForm. Otherwise I would use a Query string for a redirect to the Angular page with the token.
For CORS support need to make sure the WebApi has Cors configured for proper functionality. using the
Microsoft.AspNet.WebApi.Cors
Hope this sheds some light on the subject of how to approach this

Resources