-
Add the authentication service to the
ConfigureServices
method ofStartup.cs
:var authBuilder = services .AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; }) .AddCookie(options => { options.LoginPath = "/Login"; options.AccessDeniedPath = "/Denied"; });
-
Navigate to the
FrontEnd
project folder and rundotnet user-secrets
. It should look like:Usage: dotnet user-secrets [options] [command] Options: -?|-h|--help Show help information --version Show version information -v|--verbose Show verbose output -p|--project <PROJECT> Path to project, default is current directory -c|--configuration <CONFIGURATION> The project configuration to use. Defaults to 'Debug' --id The user secret id to use. Commands: clear Deletes all the application secrets list Lists all the application secrets remove Removes the specified user secret set Sets the user secret to the specified value Use "dotnet user-secrets [command] --help" for more information about a command.
Note: This section required you to have an app configured with Twitter. You can configure a Twitter app at http://apps.twitter.com/. For more information, see this tutorial.
-
Add the Twitter authentication services to the
ConfigureServices
method ofStartup.cs
:var twitterConfig = Configuration.GetSection("twitter"); if (twitterConfig["consumerKey"] != null) { authBuilder.AddTwitter(options => twitterConfig.Bind(options)); }
-
Add the Twitter consumer key and consumer secret keys to the user secrets store:
dotnet user-secrets set twitter:consumerSecret {your secret here} dotnet user-secrets set twitter:consumerKey {your key here}
Note: This section required you to have an app configured with Google. You can configure a Google app at https://console.developers.google.com/projectselector/apis/library. For more information, see this tutorial.
-
Add the google authentication services to the
ConfigureServices()
method:var googleConfig = Configuration.GetSection("google"); if (googleConfig["clientID"] != null) { authBuilder.AddGoogle(options => googleConfig.Bind(options)); }
-
Add the Google client key and client secret to the user secrets store:
dotnet user-secrets set google:clientID {your client ID here} dotnet user-secrets set google:clientSecret {your key here}
-
Add
app.UseAuthentication()
beforeapp.UseMvc()
inStartup.cs
.app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
-
Add a Razor Page
Login.cshtml
and a page modelLogin.cshtml.cs
to thePages
folder. -
In the
Login.cshtml.cs
get theIAuthenticationSchemeProvider
add pass the registered authentication schemes to the page via the model:using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Authentication; namespace FrontEnd.Pages { public class LoginModel : PageModel { private readonly IAuthenticationSchemeProvider _authSchemeProvider; public LoginModel(IAuthenticationSchemeProvider authSchemeProvider) { _authSchemeProvider = authSchemeProvider; } public IEnumerable<AuthenticationScheme> AuthSchemes { get; set; } public async Task<IActionResult> OnGet() { if (User.Identity.IsAuthenticated) { return RedirectToPage("/Index"); } AuthSchemes = await _authSchemeProvider.GetRequestHandlerSchemesAsync(); return Page(); } } }
-
Render all of the registered authentication schemes on
Login.cshtml
:@page @model LoginModel <h1>Login</h1> <form method="post"> @foreach (var scheme in Model.AuthSchemes) { <button class="btn btn-default" type="submit" name="scheme" value="@scheme.Name">@scheme.DisplayName</button> } </form>
-
Add code that will challenge with the appropriate authentication scheme when the button for that scheme is clicked to
Login.cshtml.cs
:public IActionResult OnPost(string scheme) { return Challenge(new AuthenticationProperties { RedirectUri = Url.Page("/Index") }, scheme); }
-
The above logic will challenge with the approriate authentication scheme and will redirect to the "/Index" page on success.
-
Add a login link to the
_Layout.cshtml
by adding the following code inside the<nav>
section in the header:<form asp-controller="account" asp-action="logout" method="post" id="logoutForm" class="navbar-right"> <ul class="nav navbar-nav navbar-right"> @if (User.Identity.IsAuthenticated) { <li><a>@Context.User.Identity.Name</a></li> <li> <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button> </li> } else { <li><a asp-page="/Login">Log in</a></li> } </ul> </form>
The updated header will look like this:
<nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a asp-page="/Index" class="navbar-brand">NDC Sydney 2017</a> </div> <div class="navbar-collapse collapse"> <form asp-controller="account" asp-action="logout" method="post" id="logoutForm" class="navbar-right"> <ul class="nav navbar-nav navbar-right"> @if (User.Identity.IsAuthenticated) { <li><a>@Context.User.Identity.Name</a></li> <li> <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button> </li> } else { <li><a asp-page="/Login">Log in</a></li> } </ul> </form> </div> </div> </nav>
-
Add a
Controllers
folder to the FrontEnd project, then and anAccountController.cs
. We won't be using scaffolding, so just add this controller by using Add/New Item / MVC Controller. Add the following code to this controller:using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Mvc; namespace FrontEnd.Controllers { public class AccountController : Controller { [HttpPost] public async Task<IActionResult> Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); return Redirect(Url.Page("/Index")); } } }
-
Add authorization service with an admin policy to the
ConfigureServices()
method ofStartup.cs
that requires an authenticated user with a specific user name from configuration.services.AddAuthorization(options => { options.AddPolicy("Admin", policy => { policy.RequireAuthenticatedUser() .RequireUserName(Configuration["admin"]); }); });
-
Add an admin username to
appSettings.json
, setting it to your Twitter or Google username. Alternatively, add it as a user secret usingdotnet user-secrets
:{ "ServiceUrl": "http://localhost:56009/", "Admin": "<username>", "Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" } }, "Console": { "LogLevel": { "Default": "Warning" } } } }
-
Add
Microsoft.AspNetCore.Authorization
to the list of usings inIndex.cshtml.cs
, then use theIAuthorizationService
in the page model to determine if the current user is an administrator.private readonly IApiClient _apiClient; private readonly IAuthorizationService _authzService; public IndexModel(IApiClient apiClient, IAuthorizationService authzService) { _apiClient = apiClient; _authzService = authzService; } public bool IsAdmin { get; set; } public async Task OnGet(int day = 0) { var authzResult = await _authzService.AuthorizeAsync(User, "Admin"); IsAdmin = authzResult.Succeeded; // More stuff here // ... }
-
On the
Index
razor page, add an edit link to allow admins to edit sessions. You'll add the following code directly after the<p>
tag that contains the sessionforeach
loop:@if (Model.IsAdmin) { <p> <a asp-page="/Admin/EditSession" asp-route-id="@session.ID" class="btn btn-default btn-xs">Edit</a> </p> }
-
Add a nested
Admin
folder to thePages
folder then add anEditSession.cshtml
razor page andEditSession.cshtml.cs
page model to it. -
Next, we'll protect this
EditSession
page it with an Admin policy by making the following change to theservices.AddMvc()
call inStartup.ConfigureServices
:services.AddMvc() .AddRazorPagesOptions(options => { options.Conventions.AuthorizeFolder("/Admin", "Admin"); });
-
Change
EditSession.cshtml.cs
to render the session in the edit form:public class EditSessionModel : PageModel { private readonly IApiClient _apiClient; public EditSessionModel(IApiClient apiClient) { _apiClient = apiClient; } public Session Session { get; set; } public async Task OnGetAsync(int id) { var session = await _apiClient.GetSessionAsync(id); Session = new Session { ID = session.ID, ConferenceID = session.ConferenceID, TrackId = session.TrackId, Title = session.Title, Abstract = session.Abstract, StartTime = session.StartTime, EndTime = session.EndTime }; } }
-
Add the "{id}" route to the
EditSession.cshtml
form:@page "{id:int}" @model EditSessionModel
-
Add the following edit form to
EditSession.cshtml
:<form method="post" class="form-horizontal"> <div asp-validation-summary="All" class="text-danger"></div> <input asp-for="Session.ID" type="hidden" /> <input asp-for="Session.ConferenceID" type="hidden" /> <input asp-for="Session.TrackId" type="hidden" /> <div class="form-group"> <label asp-for="Session.Title" class="col-md-2 control-label"></label> <div class="col-md-10"> <input asp-for="Session.Title" class="form-control" /> <span asp-validation-for="Session.Title" class="text-danger"></span> </div> </div> <div class="form-group"> <label asp-for="Session.Abstract" class="col-md-2 control-label"></label> <div class="col-md-10"> <textarea asp-for="Session.Abstract" class="form-control"></textarea> <span asp-validation-for="Session.Abstract" class="text-danger"></span> </div> </div> <div class="form-group"> <label asp-for="Session.StartTime" class="col-md-2 control-label"></label> <div class="col-md-10"> <input asp-for="Session.StartTime" class="form-control" /> <span asp-validation-for="Session.StartTime" class="text-danger"></span> </div> </div> <div class="form-group"> <label asp-for="Session.EndTime" class="col-md-2 control-label"></label> <div class="col-md-10"> <input asp-for="Session.EndTime" class="form-control" /> <span asp-validation-for="Session.EndTime" class="text-danger"></span> </div> </div> <div class="form-group"> <div class="col-md-offset-2 col-md-10"> <button type="submit" class="btn btn-primary">Save</button> <button type="submit" asp-page-handler="Delete" class="btn btn-danger">Delete</button> </div> </div> </form> @section Scripts { @Html.Partial("_ValidationScriptsPartial") }
-
Add code to handle the
Save
andDelete
button actions inEditSession.cshtml.cs
:public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } await _apiClient.PutSessionAsync(Session); return Page(); } public async Task<IActionResult> OnPostDeleteAsync(int id) { var session = await _apiClient.GetSessionAsync(id); if (session != null) { await _apiClient.DeleteSessionAsync(id); } return Page(); }
-
Add a
[BindProperty]
attribute to theSession
property inEditSession.cshtml.cs
to make sure properties get bound on form posts:[BindProperty] public Session Session { get; set; }
-
The form should be fully functional.
Add success message to form post and use the PRG pattern
-
Add a
TempData
decoratedMessage
property and aShowMessage
property toEditSession.cshtml.cs
:[TempData] public string Message { get; set; } public bool ShowMessage => !string.IsNullOrEmpty(Message);
-
Set a success message in the
OnPostAsync
andOnPostDeleteAsync
methods and changePage()
toRedirectToPage()
:public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } Message = "Session updated successfully!"; await _apiClient.PutSessionAsync(Session); return RedirectToPage(); } public async Task<IActionResult> OnPostDeleteAsync(int id) { var session = await _apiClient.GetSessionAsync(id); if (session != null) { await _apiClient.DeleteSessionAsync(id); } Message = "Session deleted successfully!"; return RedirectToPage("/Index"); }
-
Update
EditSession.cshtml
to show the message after posting. Add the following code directly below the<h3>
tag at the top:@if (Model.ShowMessage) { <div class="alert alert-success alert-dismissible" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span> </button> @Model.Message </div> }
We're currently using if
blocks to determine whether to show the login form in the header. We can clean up this code by creating a custom Tag Helper.
- Create a new folder called
TagHelpers
in the root of the FrontEnd project. Right-click on the folder, select Add / New Item... / Razor Tag Helper. Name the Tag HelperAuthzTagHelper.cs
. - Modify the
HtmlTargetElement
attribute to bind to all elements with an "authz" attribute:[HtmlTargetElement("*", Attributes = "authz")]
- Add an additional
HtmlTargetElement
attribute to bind to all elements with an "authz-policy" attribute:[HtmlTargetElement("*", Attributes = "authz-policy")]
- Inject the
AuthorizationService
as shown:private readonly IAuthorizationService _authzService; public AuthzTagHelper(IAuthorizationService authzService) { _authzService = authzService; }
- Add the following properties which will represent the
auth
andauthz
attributes we're binding to:[HtmlAttributeName("authz")] public bool RequiresAuthentication { get; set; } [HtmlAttributeName("authz-policy")] public string RequiredPolicy { get; set; }
- Add a
ViewContext
property:[ViewContext] public ViewContext ViewContext { get; set; }
- Mark the
ProcessAsync
method asasync
. - Add the following code to the
ProcessAsync
method:public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var requiresAuth = RequiresAuthentication || !string.IsNullOrEmpty(RequiredPolicy); var showOutput = false; if (context.AllAttributes["authz"] != null && !requiresAuth && !ViewContext.HttpContext.User.Identity.IsAuthenticated) { // authz="false" & user isn't authenticated showOutput = true; } else if (!string.IsNullOrEmpty(RequiredPolicy)) { // auth-policy="foo" & user is authorized for policy "foo" var authorized = false; var cachedResult = ViewContext.ViewData["AuthPolicy." + RequiredPolicy]; if (cachedResult != null) { authorized = (bool)cachedResult; } else { var authResult = await _authz.AuthorizeAsync(ViewContext.HttpContext.User, RequiredPolicy); authorized = authResult.Succeeded; ViewContext.ViewData["AuthPolicy." + RequiredPolicy] = authorized; } showOutput = authorized; } else if (requiresAuth && ViewContext.HttpContext.User.Identity.IsAuthenticated) { // auth="true" & user is authenticated showOutput = true; } if (!showOutput) { output.SuppressOutput(); } }
- Register the new Tag Helper in the
_ViewImports.cshtml
file:@namespace FrontEnd.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, FrontEnd
- We can now update the
_Layout.cshtml
method to replace theif
block with declarative code using our new Tag Helper. Update the<form>
in the nav section with the following code:<form asp-controller="account" asp-action="logout" method="post" id="logoutForm" class="navbar-right"> <ul class="nav navbar-nav navbar-right"> <li authz="true"><a>@Context.User.Identity.Name</a></li> <li authz="true"> <button type="submit" class="btn btn-link navbar-btn navbar-link">Log out</button> </li> <li authz="false"><a asp-controller="account" asp-action="login">Log in</a></li> </ul> </form>