Here Santosh Kumar Singh has explained why Server.MapPath not working in ASP.Net Core and what is the alternative solution for using the functionality in ASP.Net Core.
Wednesday, 11 November 2020
Server.MapPath not working in ASP.Net Core
Friday, 6 November 2020
Binding Dropdown List With Database In ASP.NET Core MVC
Topic
- Database part.
- Creating an Application.
- Installing package for an Entity Framework Core From NuGet.
- Adding the Connection string and Setting up DbContext.
- Adding Model CountryMaster in Models Folder.
- Adding DbSet for CountryMaster Model in DatabaseContext class.
- Adding Controller.
- Getting Data from the database, using Entity Framework Core.
- Adding View.
- Binding Dropdownlist, using new Tag helper.
- Adding Index Action method to handle POST request and getting the selected values.
- Saving and running an Application.
Step 1
Create a sample database with the name “AllSampleCode” to show demo
Inside this database, I have added a simple table, CountryMaster, and you can see the structure, as shown below.
Step 2
Now, let’s create a .NET Core Web Application. Open Visual Studio IDE from the start page and click New Project link.
Afterwards, click New Project link and it will open a new dialog with the name “New Project” inside it. From the left pane, choose templates and inside it, choose Visual C#. Afterwards, choose .NET Core template. In the middle of your pane, you will see .NET Core project templates. In templates, choose “ASP.NET Core Web Application (.NET Core)” project templates.
After choosing project template next we are going to name the project as “MVCCore1” and finally click OK button to create a project, but it will pop up another dialog with the name “New ASP.NET Core Web Application (.NET Core)”.
Inside this dialog, we are going to choose “Web application” project template to create “Web Application” and click OK button to create a project.
Below is the newly-created complete project view
After creating an Application, we are going to add the reference required for Entity Framework Core.
Step 3
To install the package just right click on the project (MVCCore1) and then select Manage NuGet package. Below dialog of NuGet Package Manager will pop up. In the browse tab, there is a search box, where you are required to type “Microsoft.EntityFrameworkCore.SqlServer” and just click Install button to install.
- Microsoft.EntityFrameworkCore.SqlServer
Step 4
After adding the reference, there is a need to add a connection string in appsetting.json file.
After adding the connection string, next step is to add a class, which will inherit DbContext class, but before doing this, let's start creating a folder for Models and inside it, we are going to add this class.
For adding a folder, just right click on the project (MVCCore1), then choose Add from Menu that pops up and inside it and choose New Folder.
Add - New Folder.
Now, let’s add a class with the name DatabaseContext in model folder.
For adding model, just right click on Models folder, then select add. Inside it, select Class and add New Item; a dialog will pop up with default class selected, followed by naming class as DatabaseContext and clicking Add button.
After adding a DatabaseContext class, we are going to inherit DbContext class.
After inheriting with DbContext, we are having a constructor, which takes DbContextOptions as an input parameter and also inherits base class constructor (: base(options)) [DbContext].
Now, we are going to add new Service in Startup.cs class to inject dependency.
Now, whenever you are going to use DatabaseContext class, DbContext instance will be injected.
After completing with add Service, we are going to add Model.
Step 5
For adding model, just right click on Models folder and select Add. Inside it, select Class and add New Item. The dialog will pop up with default class, which is selected, then we are going to name class as CountryMaster and click Add button.
Step 6
After adding CountryMaster Model, now let's add DbSet for CountryMaster Model in DatabaseContext class, as shown below.
Step 7
For adding controller, just right click on Controller folder. Select Add and inside it, select New Item. After selecting New Item, a new dialog of Add New Item will pop up.
Inside it, just choose “MVC Controller Class”, followed by naming Controller as “DemoController” and click Add button to create Controller.
Afterwards, we have clicked Add button and it creates DemoController in Controller folder, as shown below.
Step 8
In DemoController, we are using constructor injection to get a dependency.
Step 9
For adding view, right click on the Views folder. Add New Folder and name the folder “Demo”.
After adding Demo folder, now we are going to add View inside this folder.
To add view, right click on the Demo folder, which we have added and then select Add. Inside it, select New Item followed by a new dialog with Name of Add New Item will pop up.
From Add New Item dialogs, just choose “MVC View Page” to add view. In the next step, give a name to View. The View name must be the name, which is similar to Action method name, and we are going to name it “Index” [“Index.cshtml”] and click Add button to add View.
After adding a view, we are going to use new tag helper for a binding drop-down list.
Step 10
Now, save the Application and run the URL: - http://localhost:####/demo/index
Next step is to read the value, which the user will select after submitting the form.
For doing it, I have added a submit button and a @ViewBag.SelectedValue to show the selected value on view.
Complete Code Snippet of Demo.cshtml
- @model MVCCore1.Models.CountryMaster
- @{
- <form asp-controller="Home" asp-action="Demo" method="post" class="form-horizontal" role="form">
- <div class="form-group">
- <div class="row">
- <div class="alert-danger" asp-validation-summary="ModelOnly"></div>
- <div class="col-xs-12 col-sm-6 col-md-6 col-lg-4">
- <label asp-for="Name" class="control-label"></label>
- <select asp-for="ID"
- class="form-control"
- asp-items="@(new SelectList(@ViewBag.ListofCountry,"ID", "Name"))">
- </select>
- </div>
- </div>
- </div>
- <div class="form-group">
- <div class="row">
- <div class="col-xs-12 col-sm-6 col-md-6 col-lg-4">
- <input id="Submit1" type="submit" value="submit" />
- </div>
- </div>
- </div>
- <div class="form-group">
- <div class="row">
- <div class="col-xs-12 col-sm-6 col-md-6 col-lg-4">
- @if (ViewBag.SelectedValue != null)
- {
- <text>Selected Country ID: </text> @ViewBag.SelectedValue;
- }
- </div>
- </div>
- </div>
- </form>
- }
For handling Http POST request, I have added a new action method, which takes CountryMaster model as an input parameter. This model will contain the selected value of dropdown list, which the user has selected.
Now, save and run the Application followed by accessing URL - http://localhost:####/demo/index
- We are going to test validation
- Choose value from dropdown list.
- Submit the chosen value from dropdown list
- Debugging the values after submitting the form is given below.
Note
Thanks for reading the article. If you like it, please share it.
Thursday, 5 November 2020
Managing Authentication Token Expiry In WebAssembly-based Blazor
The Blazor WebAssembly project template doesn't feature an option to include authentication. If you want to add authentication to a WebAssembly-based Blazor application, you need to do it yourself. This article shows how to add application-wide authentication management and then to use built-in Razor components to protect the FetchData page in the standard template from unauthorised users.
There are some good starting points when looking at Authentication in Blazor applications. The official docs explain how to apply authentication to a Blazor Server application. On the client side, Chris Sainty has looked at managing authentication with an Identity database in one of his excellent series of Blazor articles. And Steve Sanderson (main Blazor bloke at Microsoft) provides a demo app that he showed at NDC Oslo in June this year.
Like the other examples, this article will show how to use a Web API endpoint to issue a JSON Web Token (JWT) to a validated user. Where this article builds on the other examples is in demonstrating how to manage the expiry of the token in the browser.
warning Warning
Just like input validation, client side authentication and authorisation management in Blazor can be circumvented. It is therefore very important that you properly protect server-side resources as well.
This walkthrough starts with the standard ASP.NET Core Hosted WebAssembly Blazor project:
I've called mine BlazorWasmAuthentication if you want to copy and paste code from here. The resulting solution includes 3 projects: Server, Client and Shared. Each will require amending.
Amend the Shared Project
The first changes are made to the Shared project. This is the .NET class library that holds code (mainly model classes) that are shared between the Client and Server projects. Add two classes, Credentials
and LoginResult
:
using System.ComponentModel.DataAnnotations; namespace BlazorWasmAuthentication.Shared { public class Credentials { [Required] public string Email { get; set; } [Required] public string Password { get; set; } } }
using System; namespace BlazorWasmAuthentication.Shared { public class LoginResult { public string Token { get; set; } public DateTime Expiry { get; set; } } }
Amend the Server Project
The Server project requires a few amendments. It needs to be configured to make use of ASP.NET Core authentication management with JWT bearer tokens. It also needs to provide an API that allows users to authenticate, and it needs to store the authorised user's credentials securely.
To simplify things, I won't configure an Identity database for the user credentials. Chris Sainty provides clear instructions on how to do this in his article, should you need help. The credentials for this example will be stored in an appSettings file, with the password hashed using the Identity PasswordHasher introduced in this article.
- Add an appSettings.json file to the server project with the following content:
{ "Jwt": { "Key": ITNN8mPfS2ivOqr1eRWK0Rac3sRAchQdG8BUy0pK4vQ3", "Issuer": "MyApp", "Audience": "MyAppAudience" }, "Credentials": { "Email": "user@test.com", "Password": "AQAAAAEAACcQAAAAENsLEigZGIs6kEdhJ7X1d7ChFZ4TKQHHYZCDoLSiPYy/GpYw4lmMOalsn8g/7debnA==" } }
The password has been hashed. Its original value was "test-password". - The next step is to amend project to include an additional package:
Microsoft.AspNetCore.Authentication.JwtBearer
. You can add this in any way that you prefer. The simplest way is to add a package reference to the project file:<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.0.0" />
- Next, you need to configure the application to use JWT bearer tokens. This is done in Startup, and first requires the addition of some
using
directives:using System.Text; using Microsoft.IdentityModel.Tokens; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration;
- Then you need to access the Configuration API. Inject the
IConfiguration
service into a constructor, and assign it to a public property:public Startup(IConfiguration configuration) => Configuration = configuration; public IConfiguration Configuration { get; }
- Configure authentication in
ConfigureServices
:services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = Configuration["Jwt:Issuer"], ValidAudience = Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) }; });
- Then add authentication and authorisation middleware to the request pipeline in the
Configure
method. Ensure that they are added after Routing and before EndPoint configuration:app.UseAuthentication(); app.UseAuthorization();
- Add an
[Authorize]
attribute to the existing WeatherForecast controller:namespace BlazorWasmAuthentication.Server.Controllers { [Authorize] [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase {
Remembering the warning at the top of this article, this is an important step. If you don't want unauthorised users to be able to access the information provided by the weather forecast service, it is not enough to use client side code to prevent access. Anyone with fairly basic knowledge of browser developer tools might be able to circumvent client-side restrictions. - Finally, create a Web API controller named LoginContoller:
using BlazorWasmAuthentication.Shared; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace BlazorWasmAuthentication.Server.Controllers { [ApiController] public class LoginController : ControllerBase { private readonly IConfiguration_configuration; public LoginController(IConfiguration configuration) => _configuration = configuration; [HttpPost("api/login")] public LoginResult Login(Credentials credentials) { var expiry = DateTime.Now.AddMinutes(2); return ValidateCredentials(credentials) ? new LoginResult { Token = GenerateJWT(credentials.Email, expiry), Expiry = expiry } : new LoginResult(); } bool ValidateCredentials(Credentials credentials) { var user = _configuration.GetSection("Credentials").Get<Credentials>(); var passwordHasher = new PasswordHasher<string>(); return passwordHasher.VerifyHashedPassword(null, user.Password, credentials.Password) == PasswordVerificationResult.Success; } private string GenerateJWT(string email, DateTime expiry) { var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); var token = new JwtSecurityToken( _configuration["Jwt:Issuer"], _configuration["Jwt:Audience"], new[] { new Claim(ClaimTypes.Name, email) }, expires: expiry, signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256) ); var tokenHandler = new JwtSecurityTokenHandler(); return tokenHandler.WriteToken(token); } } }
For the purposes of demonstration, the token expiry is set to 2 minutes. This is so that you can test expiry without growing old. The Web API entry point validates the credentials. In this example, the code simply reads the credentials stored in the configuration file and compares them to the posted values. If they are valid, aLoginResult
is returned complete with a token and an expiry. Otherwise an emptyLoginResult
is returned. The code for generating the token is pretty much boilerplate, and lifted directly out of Steve Sanderson's demo.
The Client Application
Authentication management in the client application relies on two principal actors: a class that derives from AuthenticationStateProvider
, implementing its GetAuthenticationStateAsync
method; and a CascadingAuthenticationState
component. The CascadingAuthenticationState
component obtains the current authentication state of the user by subscribing to the AuthenticationStateProvider
's AuthenticationStateChanged
event. Then the CascadingAuthenticationState
component makes that information available to children via a cascading value of type Task<AuthenticationState>
. The AuthenticationStateProvider
is responsible for setting the authentication status of the user.
- Start by adding a package reference to
Microsoft.AspNetCore.Components.Authorization
in the Client project's csproj file:<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0-preview1.19508.20" />
- Add a
using
directive to the _Imports.razor file to bring the contents of the package into scope along with the ASP.NET Core authentication package:@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization
- Add a folder named AuthenticationStateProviders, and inside it, add a C# class file named TokenAuthenticationStateProvider.cs with the following code:
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.JSInterop; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; namespace BlazorWasmAuthentication.Client.AuthenticationStateProviders { public class TokenAuthenticationStateProvider : AuthenticationStateProvider { private readonly IJSRuntime_jsRuntime; public TokenAuthenticationStateProvider(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public async Task SetTokenAsync(string token, DateTime expiry = default) { if (token == null) { await _jsRuntime.InvokeAsync<object>("localStorage.removeItem", "authToken"); await _jsRuntime.InvokeAsync<object>("localStorage.removeItem", "authTokenExpiry"); } else { await _jsRuntime.InvokeAsync<object>("localStorage.setItem", "authToken", token); await _jsRuntime.InvokeAsync<object>("localStorage.setItem", "authTokenExpiry", expiry); } NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public async Task<string> GetTokenAsync() { var expiry = await _jsRuntime.InvokeAsync<object>("localStorage.getItem", "authTokenExpiry"); if(expiry != null) { if(DateTime.Parse(expiry.ToString()) > DateTime.Now) { return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "authToken"); } else { await SetTokenAsync(null); } } return null; } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var token = await GetTokenAsync(); var identity = string.IsNullOrEmpty(token) ? new ClaimsIdentity() : new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"); return new AuthenticationState(new ClaimsPrincipal(identity)); } private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt) { var payload = jwt.Split('.')[1]; var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); return keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())); } private static byte[] ParseBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } } }
This code is largely based on the Mission Control demo. TheAuthenticationStateProvider
includes aSetTokenAsync
method and aGetTokenAsync
method. TheSetTokenAsync
method uses Blazor's JavaScript interop service to use the browser's local storage feaure to store the token, if one is provided. It also stores the token's expiry time. If no token is provided, the method removes both the storage keys related to the token and its expiry time, effectively logging the user out. Finally, the method callsNotifyAuthenticationStateChanged
, which raises theAuthenticationStateChanged
event that theCascadingAuthenticationState
component subscribes to, updating theCascadingAuthenticationState
component about the current authentication status of the user.
TheGetTokenAsync
method checks the expiry time of the token. If the expiry time has expired, theSetToken
method is called without a token being provided, logging the user out. Otherwise a valid token is returned, if one exists.
The final public method, which must be overridden in classes that derive fromAuthenticationStateProvider
, is theGetAuthenticationStateAsync
method. This method parses the JSON Web Token and creates aClaimsPrincipal
(representing the current user) with either the identity information (ClaimsIdentity
) obtained from the token, or an emptyClaimsIdentity
if no token exists.info The method for parsing the JWT is taken from the Mission Control demo. JWTs contain three parts: a header, a payload (the source of theClaimsIdentity
information) and a signature. Each part is Base64 Url encoded and then the parts are joined using dots. The final output e.g.header.payload.signature
forms the token. When using Base64 Url encoding, output padding is optional, and in fact is not included in the generation of JWTs. TheSystem.Convert.FromBase64String
method expects the input string to have output padding where necessary, and will raise aFormatException
if it is missing. Therefore the additional private method at the end of the class is used to put padding characters (=
) on to the end of the payload if they are needed before the string is decoded. - The
AuthenticationStateProvider
needs to be registered with the dependency injection system. This is done in theConfigureServices
method in Startup. Add authentication services to the application too:public void ConfigureServices(IServiceCollection services) { services.AddAuthorizationCore(); services.AddScoped<TokenAuthenticationStateProvider>(); services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<TokenAuthenticationStateProvider>()); }
TheTokenAuthenticationStateProvider
is registered so that it can be injected directly into components etc, and then the injected service is registered as the implementation ofAuthenticationStateProvider
. This isn't necessarily a recommended pattern. But it makes things simpler for demo purposes. If you want to adopt a more robust approach, move the methods that get and set tokens from theTokenAuthenticationStateProvider
into a separate service and use that where this demo explicitly injects theTokenAuthenticationStateProvider
. Check Chris Sainty's AuthService for some inspiration. - The next step involves creating the Login form. Add a new Razor Component to the Pages folder named Login.razor wih the following code:
@inject HttpClient Http @inject TokenAuthenticationStateProvider AuthStateProvider <div class="container col-6"> @if (loginFailure) { <div class="alert alert-danger">Your credentials did not work. Please try again.</div> } <div class="card"> <div class="card-body"> <h5 class="card-title">Login</h5> <EditForm @ref="loginform" Model="credentials" OnValidSubmit="SubmitCredentials"> <DataAnnotationsValidator /> <div class="form-group">> <label>Email address</label> <InputText class="form-control" @bind-Value="credentials.Email" /> <ValidationMessage For="@(()=> credentials.Email)" /> </div> <div class="form-group"> <label>Password</label> <InputText type="password" class="form-control" @bind-Value="credentials.Password" /> <ValidationMessage For="@(()=> credentials.Password)" /> <div/> <button type="submit" class="btn btn-outline-primary btn-sm">Submit</button> </EditForm> </div> </div> </div> @code { Credentials credentials = new Credentials(); bool loginFailure; EditForm loginform { get; set; } async Task SubmitCredentials() { var result = await Http.PostJsonAsync<LoginResult>("api/login", credentials); loginFailure = result.Token == null; if (!loginFailure) { await AuthStateProvider.SetTokenAsync(result.Token, result.Expiry); } } }
There is not much to explan here. If the form validation succeeds, theSubmitCredentials
method is called. If the login is successful (indicated by the presence of a token in the response from the LoginController), the injectedTokenAuthenticationStateProvider
sets the token, which as you remember, results in the authentication status being updated with any component that subscribes to theNotifyAuthenticationStateChanged
event. - Now it's time to introduce the component that does subscribe to the
NotifyAuthenticationStateChanged
event, theCascadingAuthenticationState
component. Open the App.razor file and replace the existing content with the following:<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> <Login/> </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
First, you added the wrapped the entire application in theCascadingAuthenticationState
component, ensuring that any other application component is able to receive itsTask<AuthenticationState>
cascading value as a parameter. You changed theRouteView
component for anAuthorizeRouteView,
which does the same except that it only displays the content of the page if the user is authenticated. If the user is not authenticated, the child content of theNotAuthorized
component is displayed, i.e. the login component that you just created. - Change the top of the FetchData.razor file to look like this:
@page "/fetchdata" @using BlazorWasmAuthentication.Shared @using System.Net.Http.Headers; @inject HttpClient Http @inject TokenAuthenticationStateProvider TokenProvider @attribute[Authorize] <h1>Weather forecast</h1>
You changes involve the addition of a using directive to bringSystem.Net.Http.Headers
into scope; you injected theTokenAuthenticationStateProvider
; and you added an[Authorize]
attribute to the page. If you try to run the page at this stage, you should see the login form that you created: - Now amend the
@code
block in Fetchdata as follows:@code { private WeatherForecast[] forecasts; protected override async Task OnInitializedAsync() { var token = await TokenProvider.GetTokenAsync(); if (token != null) { Http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); forecasts = await Http.GetJsonAsync<WeatherForecast[]>("WeatherForecast"); } } }
The existing code has been altered to obtain the JWT token and then add it to the api request for weather forecast data as a request header. Without this, there is no way for the API to authenticate the user. Remember, theGetTokenAsync
method will log the user out if the token has expired. If that happens, the user will be presented with the login form again. - Open the MainLayout.razor file and replace the About link with the following code:
<AuthorizeView>Logged in as @context.User.Identity.Name <button class="btn btn-sm btn-outline-dark" @onclick="@(() => TokenProvider.SetTokenAsync(null))">Logout</button> </AuthorizeView>
This last step completes the demo. Ensuring that the Server project is set as the Startup project, run the application in the browser. Navigate to the FetchData page and log in. You should see the data, and the message at the top of the page telling you that you are logged in together with a log out button. Click it, and you should be presented with the login form again. This time, after you have logged in, wait for a couple of minutes. Then refresh the page. You should get logged out and presented with the login form again.