How can WebApi ChallengeResult be incorporated into an Angular2 SPA? - c#

Context
We are migrating a web application using Asp.Net Core (Web Api) to use an Angular2
Single Page Application (SPA) front end. The application was initially made with "Individual User Accounts". We have been converting the cshtml views to Angular2 components, the controller methods to produce Json, even incorporating JWT for authentication.
I have been using this Configuring Google authentication microsoft documentation.
Problem
The issue is illustrated by the following two controller methods. The LinkLogin method returns a ChallengeResult action. Obviously this cannot be converted to JSON. I presume this result is supposed to cause the browser to redirect to the external login provider, then automatically redirect to LinkLoginCallback.
[HttpPost]
public async Task<IActionResult> LinkLogin([FromBody] string provider)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(externalCookieScheme);
// Request a redirect to the external login provider to link a login for the current user
string redirectUrl = Url.Action(nameof(LinkLoginCallback), "Manage");
var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, userManager.GetUserId(User));
return Challenge(properties, provider);
}
[HttpPost]
[Produces("application/json")]
public async Task<ActionResult> LinkLoginCallback()
{
User user = await userManager.GetUserAsync(User);
if (user == null)
return Json(false);
ExternalLoginInfo info = await signInManager.GetExternalLoginInfoAsync(await userManager.GetUserIdAsync(user));
if (info == null)
return Json(false);
IdentityResult result = await userManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.Authentication.SignOutAsync(externalCookieScheme);
return Json(true);
}
return Json(false);
}
If I do attempt to call LinkLogin from Angular I get this response:
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:53462' is therefore not allowed access. The response had HTTP status code 405.

So I ended up coming up with a solution that avoids "ChallengeResult" by modifying the flow between Angular2 and the WebApi.
Added the angular2-social-login npm package to the angular app (following all their easy instructions for setup).
Merged both controller methods into one. The ExternalLoginViewModel parameter is a simple class with Email, ProviderKey and LoginProvider properties (populated in Angular in step 3):
[HttpPost]
[Produces("application/json")]
public async Task<ActionResult> LinkLogin([FromBody] ExternalLoginViewModel info)
{
if (ModelState.IsValid)
{
User user = await userManager.GetUserAsync(User);
if (user != null)
{
IdentityResult result = await userManager.AddLoginAsync(user, new UserLoginInfo(info.LoginProvider, info.ProviderKey, info.LoginProvider));
if (result.Succeeded)
return Json(true);
}
}
return Json(false);
}
Changed the Angular2 client to use the social media package to get the credentials needed for the web api. Here is my LinkProvider method demonstrating the connection: (this.service.LinkLogin calls the LinkLogin controller method)
public async LinkProvider(providerName: string): Promise<void>
{
const result: any = await this.auth.login(providerName).toPromise();
const success: boolean = await this.service.LinkLogin({
email: result.email,
loginProvider: result.provider,
providerKey: result.uid
});
// Handle link response here
}

Related

Angular and .Net Core - User isn't authenticated CLAIMS

I'm developing an application in Angular and .Net, it consists of creating projects -like Asana- each project has a creator, so there I made the connection, I related the table of users and projects to create the foreign key (userCreationId).
It's a POST request, this is my service:
public string ObtenerUsuarioId()
{
if (httpContext.User.Identity.IsAuthenticated)
{
var idClaim = httpContext.User.Claims
.Where(x => x.Type == ClaimTypes.NameIdentifier).Single();
return idClaim.Value;
}
else
{
throw new Exception("El usuario no está autenticado");
}
}
And the controller:
public async Task<IActionResult> Post([FromBody] ProyectoCreacionDTO proyectoCreacionDTO)
{
var proyecto = mapper.Map<Proyecto>(proyectoCreacionDTO);
var usuarioId = servicioUsuarios.ObtenerUsuarioId();
proyecto.UsuarioCreacionId = usuarioId;
context.Add(proyecto);
await context.SaveChangesAsync();
return Ok(proyecto);
}
But the request in Angular does not bring me the claims, and it falls in the else of my service in .net.
It doesn't read the email and the id.
error : "System.Exception: User isn't authenticated \r\n.
Which is the else of my service.
I tried the request on POSTMAN and it works:
It does read the email!
And I get the answer I was expecting:
A successful answer
But then again when I tried this in Angular it doesn't work, and I'm saving the token in LocalStorage
login(email:string, password:string):Observable<Login>{
return this.http.post<Login>(`${this.url}/login`, {email, password})
.pipe(
tap(resp => {
this.localStorage.getToken(resp.token)
})
)
crearProyecto(proyecto:Proyecto):Observable<Proyecto>{
return this.http.post<Proyecto>(`${this.url}/agregar`, proyecto)
The token is actually saved, I can protect routes with AuthGuards, so I dont know why when I send the request from Angular it doesn't work
Here is the token
I even have the Cookie that Identity generates:
The cookie

In my Asp .net 2.1 ADFS implementation, GetExternalLoginInfoAsync in my account controller always returns null

When a user goes through the ADFS flow and I get the callback to my /account/externalLogin in my account controller.
The line:
var info = await _signInManager.GetExternalLoginInfoAsync(); always sets info to null.
This is the case on both our internal adfs test instance and our client's test instance. (This project is to add SSO support to our client)
The Code below is from my account controller and gets called after the user goes through the adfs flow. The if statement info == null is always true and we redirect them to the help page as a testing location.
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLogin(string returnUrl = null, string page = null, string handler = null, string remoteError = null)
{
// Catches the first get request from someone returning from external login
Console.WriteLine("Login event!!!");
if (remoteError != null)
{
ViewBag.ErrorMessage = $"Error from external provider: {remoteError}";
Console.WriteLine($"Error from external provider: { remoteError}");
return RedirectToAction("Help", "Home");
}
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
Console.WriteLine("no info, retrying");
return RedirectToAction("Help", "Home");
}
The relevant section from my startup.cs file is:
services.AddAuthentication()
.AddWsFederation(options =>
{
// MetadataAddress represents the Active Directory instance used to authenticate users.
options.MetadataAddress = "https://adfsqa.client.com/FederationMetadata/2007-06/FederationMetadata.xml";
//options.MetadataAddress = "https://ourtestadfsinstance.westus2.cloudapp.azure.com/FederationMetadata/2007-06/FederationMetadata.xml";
// Wtrealm is the app's identifier in the Active Directory instance.
// For ADFS, use the relying party's identifier, its WS-Federation Passive protocol URL:
options.Wtrealm = "https://rc.dm1.tech";
});
My implementation is very simple and close the stock example for implementing ADFS documented here. Implementation details were also taken from this sample from the ASP .net core git repo sample project for ExternalClaims here.
I searched for this issue extensively and spend several days trying to solve it at this point. The closest public post of this issue appears to be this one but most of the other are around rolling out Oauth2 from steam, Instagram or any other 3rd party.
Any clues, hints, or even thoughts would be greatly appreciated.

Identity server 4 not removing cookie

I have front app on angular 5 and backend api on c# using identity server.
The problem is that when I click logout button, the token is removed and i am redirected to logout page.
But when I try to refresh main page, I am redirected to microsoftonline.com
authenticated automatically and redirected back to main page
I am missing providing username and password here, and this occurs in chrome incognito.
What I noticed is that if I remove manually the cookie from microsoftonline.com
and repeat the process, this time I will be asked for username and password.
So first I tried to clean all cookies this way but it din't help
foreach (var key in HttpContext.Request.Cookies.Keys)
{
HttpContext.Response.Cookies.Append(key, "", new CookieOptions() { Expires = DateTime.Now.AddDays(-1) });
}
bellow is my accountcontroller logout method and cookie screen
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutViewModel model)
{
var idp = User?.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
var subjectId = HttpContext.User.Identity.GetSubjectId();
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
{
if (model.LogoutId == null)
{
model.LogoutId = await interaction.CreateLogoutContextAsync();
}
try
{
await signInManager.SignOutAsync();
}
catch (NotSupportedException)
{
}
}
// set this so UI rendering sees an anonymous user
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
// get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await interaction.GetLogoutContextAsync(model.LogoutId);
var vm = new LoggedOutViewModel
{
PostLogoutRedirectUri = logout?.PostLogoutRedirectUri,
ClientName = logout?.ClientId,
SignOutIframeUrl = logout?.SignOutIFrameUrl
};
await persistedGrantService.RemoveAllGrantsAsync(subjectId, "angular2client");
return View("LoggedOut", vm);
}
If I understand correctly you are federating to Microsoft from your IdentityServer4 service? If so when you sign out of your identity service you should also give the user the option to sign out of the external provider (if it supports the relevant feature - it'd need to define an end_session_endpoint in the discovery document).
This functionality is supported by the standard OIDC middleware so you should be able to initiate signout by calling SignoutAsync() and passing the name of the scheme for the MS federated sign in.
Another option is to always send prompt=login in your external sign in requests and then check the auth_time claim you get back. That way to you force interactive sign in always and also verify when it happened.
Try cleaning the cookies from the HttpContext itself, using the extension method, provided by Identity Server, like here.
Or try this:
await HttpContext.SignOutAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme);
in your Logout controller method.
3rd option (what I have in one of my test MVC clients is):
public ActionResult Logout()
{
Request.GetOwinContext().Authentication.SignOut();
return Redirect("/");
}
public void SignoutCleanup(string sid)
{
var cp = (ClaimsPrincipal)User;
var sidClaim = cp.FindFirst("sid");
if (sidClaim != null && sidClaim.Value == sid)
{
Request.GetOwinContext().Authentication.SignOut("Cookies");
}
}
Where the Logout method is called on the button click, and the SignoutCleanup is the one that is passed to Identity Server, when registering the client as a Client.BackChannelLogoutUri (or Client.FrontChannelLogoutUri, or both, depending on your scenario).
PS: Now, in general I think that your approach is not right, but I don't know your full case, so I'm not judging you - just giving and advice.
For front-end clients (Angular, Vue, vanilla JS etc..) it is recommended to use the client-side oidc-client-js library. And here is the usage example. As I said - this is just an advice, but if you are in the very beginning of your authentication setup, I would recommend you to have a look.

Openiddict. Unable to reach Accept(Deny) authorization endpoint for Authorization Code flow

I use the Openiddict library for OAuth2 authentification on the server side. We use Asp.Net Core - server side, Angular 2 - client side. After processed Authorize request, the server redirects to the following URL with user confirmation form. See example code below:
[Authorize]
[HttpGet]
[Route(RouteConst.OAuth.AUTHORIZE)]
[EnableCors(CommonConst.ALLOW_ALL)]
public async Task<IActionResult> Authorize()
{
var request = this.HttpContext.GetOpenIdConnectRequest();
// Retrieve the application details from the database.
var application = await this._applicationManager.FindByClientIdAsync(request.ClientId, this.HttpContext.RequestAborted);
if (application == null)
{
return this.BadRequest(new ErrorUI
{
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
return this.Redirect($"/app/profile?oauth={request.Scope}&application={application.DisplayName}&requestId={request.RequestId}");
}
After redirect user can see the confirmation form then click Accept(Deny) button. In Openiddict Code samples is used MvcBinders and Razor views, but when I tried to create similar POST request to the server from Angular view - OpenIdConnectRequest is null.
Why is OpenIdConnectRequest null after redirect to the local URL? Can I reach the Accept Authorize endpoint within CORS POST request without using MVC?
Startup setup:
services.AddOpenIddict(options =>
{
options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
options.EnableAuthorizationEndpoint(RouteConst.OAuth.AUTHORIZE)
.EnableLogoutEndpoint(RouteConst.OAuth.LOGOUT)
.EnableTokenEndpoint(RouteConst.OAuth.TOKEN)
.EnableUserinfoEndpoint(RouteConst.USERINFO);
options.AllowAuthorizationCodeFlow();
options.EnableRequestCaching();
});
Accept endpoint:
[Authorize]
[HttpPost]
[Route(RouteConst.OAuth.AUTHORIZE)]
[FormValueRequired("submit.Accept")]
public async Task<IActionResult> Accept()
{
var request = this.HttpContext.GetOpenIdConnectRequest();
// Retrieve the profile of the logged in user.
var user = await this._userManager.GetUserAsync(this.User);
if (user == null)
{
return this.BadRequest(new ErrorUI
{
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new authentication ticket.
var ticket = await this.CreateTicketAsync(request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return this.SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}

Token-based Authorization in Existing ASP.NET MVC App

I have inherited an existing application. This application uses ASP.NET MVC 3. It has some APIs. Those APIs look like the following:
[AcceptVerbs(HttpVerbs.Post)]
[Endpoint]
public ActionResult AuthenticatePlayer(string username, string password)
{
// Ensure that the user entered valid credentials
if (Membership.ValidateUser(username, password) == false)
return Json(new { statusCode = StatusCodes.INVALID_CREDENTIALS, message = "You entered an invalid username or password. Please try again." });
// Get the profile of the person that just logged in.
ProfileCommon userProfile = (ProfileCommon)(ProfileCommon.Create(username));
if (userProfile != null)
{
string name = username;
if (String.IsNullOrEmpty(userProfile.FirstName) == false)
name = userProfile.FirstName;
return Json(new {
statusCode = StatusCodes.SUCCESS,
payload = name,
username = username.ToLower(),
});
}
}
[AcceptVerbs(HttpVerbs.Get)]
[Endpoint]
public ActionResult SomeUserAction(string q)
{
// TODO: Ensure the user is authorized to perform this action via a token
// Do something
return Json(new { original = q, response = DateTime.UtcNow.Millisecond }, JsonRequestBehavior.AllowGet);
}
I'm trying to figure out how to integrate a token-based authorization schema into this process. From my understanding, a token-based system would return a short-lived token and a refresh token to a user if they successfully login. Then, each method can check to see if a user is authorized to perform the action by looking at the token. I'm trying to learn if this is built-in to ASP.NET MVC or if there is a library I can use. I need to figure out the shortest way to get this done.
Thank you so much!
I've built a WebAPI Token Authentication library a year ago, providing Token based authentication:
WebAPI Token Auth Bootstrap is out of the box Token based User Auth for WebAPI applications, Provides ready to use 'TokenAuthorize'
Attribute and 'TokenAuthApiController' Controller.
Among its features - Token Based User Authentication User Property inside the
TokenAuthApiController (Id, Username, Role, LastAccess).
Token Based User Authorization TokenAuthorizeAttribute with Access
Level - Public, User, Admin or Anonymous.
Built-in Functionality Login(), Logoff(), Error(), Unauthorized()
Responses with various overloads.
You can read more about here and in its own wiki in GitHub.
Nowadays I am working on a Node.js application and I am using Json Web Tokens (JWT) using Node.js library and it is very easy and straightforward.. its Node.js after all ;)
I saw there is a .NET implementation of JWT explained on this article which I recommend you to look at.
You can use Owin ... i.e. Microsoft.owin.security
I haven't tried this implementation but this is just to give you an idea:
var identity = new ClaimsIdentity(Startup.OAuthBearerOptions.AuthenticationType);
var currentUtc = new SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromMinutes(30));
DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return Json(new {
statusCode = StatusCodes.SUCCESS,
payload = name,
username = username.ToLower(),
accessToken = Startup.OAuthBearerOptions.AccessTokenFormat.Protect(ticket)
});

Categories

Resources