Calling WebAPI with authorisation from server side blazor - authorization

Apologies if this is a bit of a dumb question - I'm trying to get my head round security setup and don't have much experience in this area. Been reading as much as I can, but can't find a clear example for what I'm trying to do.
I have created the default server-side and wasm blazor projects from the visual studio templates, and have shared the wasm project so I can re-use both client-side and server-side as per Carl Franklin's article:
http://www.appvnext.com/blog/2020/2/2/reuse-blazor-wasm-ui-in-blazor-server
That all works fine.
Next, I repeat, but add the "Individual User Accounts" option to both projects when creating, set the db string to a shared identity database. Both work individually, however, when I share the client code, and call from server-side blazor, the webapi call fails with an "unauthorized" error.
So, in short, I'm logging into the server-side blazor project successfully. The failure happens when I attempt to call the webapi which now sits in a separate project (the WASM project) and so will run in a different domain (don't think I'm hitting cors problems yet). When I attempt to call the webapi I get the unauthorized error. When I run in WASM, it all works as expected.
Can someone please give me a pointer on what steps I need to take to get this working? Full code for the razor component is below...
#page "/fetchdata"
#using Microsoft.AspNetCore.Authorization
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#using BlazorWasm.Shared
#attribute [Authorize]
#inject HttpClient Http
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
#if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
#foreach (var forecast in forecasts)
{
<tr>
<td>#forecast.Date.ToShortDateString()</td>
<td>#forecast.TemperatureC</td>
<td>#forecast.TemperatureF</td>
<td>#forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
#code {
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
try
{
string url = "https://localhost:44378/WeatherForecast";
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>(url);
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
catch(HttpRequestException exception)
{
string msg = exception.Message;
}
}
}

Related

.NET CORE and ADFS no Claims Available

Migrating to .NET Core 3 from a 4.6 project and I'm not 100% sure I am implementing things properly.
I followed the steps in this article, making sure to configure startup.cs following the code sample under the "Use WS-Federation without ASP.NET Core Identity" section.
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/ws-federation?view=aspnetcore-3.0
Login seems to be working in that I'm redirected to MS login and sent back to my app with the expected cookies, for example MSISAuthenticated. But, user.identity.isauthenticated is always false and I have no claims available. Is this expected behavior? Perhaps I'm not configuring things properly? Ideally I'd like to be able to check if a user is authenticated and access the claims.
I've come across a number of articles about adding policies based on groups, but how would [Authorize (Policy="SomeGroup")] even work if no claims are available?
ConfigureServices Code:
enter image description here
Configure Code:
enter image description here
Controller Action:
public IActionResult Index()
{
var identity = (ClaimsIdentity)User.Identity;
ViewBag.Claims = identity.Claims;
return View();
}
View Code:
#using System.Security.Claims;
#{
ViewBag.Title = "Home Page";
IEnumerable<Claim> claims = (IEnumerable<Claim>)ViewBag.Claims;
}
#if (User.Identity.IsAuthenticated)
{
<div class="jumbotron">
<h1>Successful Sign On!</h1>
</div>
<div class="row">
<div class="col-md-12">
<h2>WS Federation Services Claims</h2>
#foreach (Claim claim in claims)
{
<p>
<b>#(claim.Type.ToString())</b>
<br />
#(claim.Value.ToString()) (type: #(claim.ValueType.ToString()))
<hr />
</p>
}
</div>
</div>
}
else
{
<div class="jumbotron">
<h1>SSO Test</h1>
<p class="lead">To sign in using Microsoft's single sign-on service, click the button below.</p>
<p>Sign in »</p>
</div>
}
perhaps the fact is that you are not send the desired ResourceUrl to ADFS. Then ADFS considers the default resource and issues a token without claims.
See more info on 3 step in "High level AD FS authentication flow"
enter link description here
AD FS identifies the resource which the client wants to access through
the resource parameter passed in the auth request. If using MSAL
client library, then resource parameter is not sent. Instead the
resource url is sent as a part of the scope parameter: scope =
[resource url]//[scope values e.g., openid].

Blazor A second operation started on this context before a previous operation completed

I makes the NavMenu dynamically and return menu i the database by users and in the index page already i returned something in the database but when i run the application or reload it show me bellow error
InvalidOperationException: A second operation started on this context
before a previous operation completed. This is usually caused by
different threads using the same instance of DbContext. For more
information on how to avoid threading issues with DbContext, see
https://go.microsoft.com/fwlink/?linkid=2097913.
NavMenu code,
List<Menu> menus = new List<Menu>();
protected override async Task OnInitializedAsync()
{
menus = await MenuService.GetMenus();
}
Index code
#if (priorities == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
#foreach (var priority in priorities)
{
<tr>
<td>#priority.Name</td>
</tr>
}
</tbody>
</table>
}
#code {
List<Priority> priorities;
protected override async Task OnInitializedAsync()
{
priorities = await PriorityService.GetPriorities();
}
}
The solution is to use a `DbContextFactory :
Quoting docs:
Some application types (e.g. ASP.NET Core Blazor) use dependency injection but do not create a service scope that aligns with the desired DbContext lifetime. Even where such an alignment does exist, the application may need to perform multiple units-of-work within this scope. For example, multiple units-of-work within a single HTTP request.
In these cases, AddDbContextFactory can be used to register a factory for creation of DbContext instances. For example:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextFactory<ApplicationDbContext>(
options =>
options.UseSqlServer(#"Server=(localdb)\mssqllocaldb;Database=Test"));
}
I got this error only when I attempted to access the user's information from the same service in multiple controls. Makes sense-- they are both fighting for the same resource on initialization.
For me, the solution wasn't to change the scope-- it was to re-think what I was really doing-- why have 2 separate controls trying to access Membership features for the same user at all?
My solution for a Blazor server app was to make the ApplicatonDbContext and the service both Transient:
services.AddDbContext<ApplicationDbContext>(x => x.UseSqlServer(connectionString), ServiceLifetime.Transient);
services.AddTransient<ICustomerService, CustomerService>();

NullReferenceException when accessing #User.Identity.Name

We are running an ASP net core 2.1 website. In the header of the page, we want to display the name of the current user. The variable is retrieved through the windows authentication scheme using the #User.Identity.Name reference and is assembled like this: ZH_MB\rohzeh, where ZH_MB is the domain and rohzeh the windows AD user. Because we just want the username, we split both loike this:
<p class="pull-right navbar-text"><span class="glyphicon glyphicon-user"></span> #User.Identity.Name.Split("\\")[1]</p>
Now when we run this code, we get the following error:
An unhandled exception occurred while processing the request.
NullReferenceException: Object reference not set to an instance of an
object. AspNetCore.Views_Shared__Layout.b__46_1() in
_Layout.cshtml, line 111
Line 111 is the code above. When I run it without the split part like this:
<p class="pull-right navbar-text"><span class="glyphicon glyphicon-user"></span> #User.Identity.Name</p>
It works just fine, except for the domain information that I don't want.
When i surround this code with a try/catch block, it works as expected, showing only the user name:
#try
{
<p class="pull-right navbar-text"><span class="glyphicon glyphicon-user"></span> #User.Identity.Name.Split("\\")[1]</p>
}
catch (NullReferenceException) { }
Any idea what the problem might be? My initial idea was: the information is just not ready when the page is rendered. But in this case, the second line of could should provide the same error.
It seems
your user is not authenticated yet or Name is null
or you didn't install ASP.NET Core/.NET Core: Runtime & Hosting Bundle for IIS
or you didn't configure all needed to use Windows Authentication
or forwarding Windows Authentication is set to false for some reasons
You can set it this way or others:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<IISOptions>(options => options.ForwardWindowsAuthentication = true);
// Add framework services.
services.AddMvc();
}
Thanks, the answer of Chris Pratt did the trick. I could replace the try/catch with the "?" and the username is shown corrently.

How do you create a confirmation message in .net Core 2.1 RazorPages?

Hopefully not a dumb question- I am rewriting an app from .net core mvc to .net core Razor. In MVC I use viewbags to create and display confirmation of actions being successful or display error message if not. Viewbags don't seem to be used or available in the same way for Razor pages in .net core 2.1.
How do you achieve the above in Razor pages? Any code snippets as example would be helpful. Thanks
We can use a Post-Redirect-Get pattern to display a message after an action.
Here is an example that uses TempData to store a message during a POST and then redirects to a GET. Using TempData to store the message is particularly appropriate for redirection, because the data only exists until something reads it.
SomePage.cshtml
#page
#model SomePageModel
#if(TempData[SomePageModel.MessageKey] is string message)
{
<p>#message</p>
}
<form method="POST">
<button type="submit">POST!</button>
</form>
SomePage.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace temp.Pages
{
public class SomePageModel : PageModel
{
public const string MessageKey = nameof(MessageKey);
public void OnGet() { }
public IActionResult OnPost() {
TempData[MessageKey] = "POST Success!";
return RedirectToAction(Request.Path); // redirect to the GET
}
}
}
This pattern also works for HTTP methods such as PUT and DELETE. Simply substitute any other HTTP verb; for instance, we can do a Put-Redirect-Get.

How to access a WCF service in an ASP.Net MVC application?

I have question about the way to access the WCF. I built a secure WCF service that returns data from a data base and it works fine. Now I need to access this web service through MVC (I do not have enough knowledge about it).
I checked similar questions on Stack Overflow but I did not find what I need. I followed this link but as I said, WCF returns data from SQL, I connect my WCF with SQL and when I used this example I don't get the expected result.
the operation that i invoke in MVC and it return dataset type from SQL
[OperationContract]
DataSet GetAllbooks(string Title)
in Homecontrller in MVC i wrote
ServiceReference1.Service1Client obj = new ServiceReference1.Service1Client();
public ActionResult Index()
{
DataSet ds = obj.GetAllbooks();
ViewBag.AuthorList = ds.Tables[0];
return View();
}
and in view i wrote
#{
ViewBag.Title = "AuthorList";
}
<table>
<tr><td>ISBN</td><td>Author</td><td>Price</td></tr>
<%foreach (System.Data.DataRow dr in ViewBag.AuthorList.Rows)
{%>
<tr>
<td><%=dr["ISBN"].ToString()%></td>
<td><%=dr["Author"].ToString() %></td>
<td><%=dr["Price"].ToString() %></td>
</tr>
<% } %>
</table>
i don't get any result
Also some services that provide by WCF need to accept input from user how i can do it
Thank you.
This is a pretty basic question but generally speaking you can add a web service reference and endpoint info in the main Web.Config file but I suspect you are having trouble with calling the WCF service URL, if so I posted an example of a generic class/wrapper for calling WCF web services in an MVC application.
Add the Web Reference to Visual Studio 2012:
Right click the project in the Solution Explorer
Choose Add–> Service Reference –> Then click the Advanced Button... –>
Then click the "Add Web Reference…" button –> then type the address of your Web Service into the URL box. Then click the green arrow and Visual Studio will discover your Web Services and display them.
You may have known the above already and might just need a generic wrapper class which makes calling the WCF Web Service easy in MVC. I've found that using the generic class works well. I can't take credit for it; found it on the internet and there was no attribution. There is a complete example with downloadable source code at http://www.displacedguy.com/tech/powerbuilder-125-wcf-web-services-asp-net-p3 that calls a WCF Web service that was made using PowerBuilder 12.5.Net, but the process of calling the WCF Web service in MVC is the same no matter if it was created in Visual Studio or PowerBuilder.
Here is the code for a generic wrapper class for calling WCF Web Services in ASP.NET MVC
Of course don't model your error handling after my incomplete example...
using System;
using System.ServiceModel;
namespace LinkDBMvc.Controllers
{
public class WebService<T>
{
public static void Use(Action<T> action)
{
ChannelFactory<T> factory = new ChannelFactory<T>("*");
T client = factory.CreateChannel();
bool success = false;
try
{
action(client);
((IClientChannel)client).Close();
factory.Close();
success = true;
}
catch (EndpointNotFoundException e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
catch (CommunicationException e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
catch (TimeoutException e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
catch (Exception e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
finally
{
if (!success)
{
// abort the channel
((IClientChannel)client).Abort();
factory.Abort();
}
}
}
}
}