Skip to content

Latest commit

 

History

History
581 lines (494 loc) · 19.9 KB

4. Add auth features.md

File metadata and controls

581 lines (494 loc) · 19.9 KB

Add ability to log in to the website

Add Authentication services

  1. Add the cookie authentication services by adding the following code to the ConfigureServices() method of Startup.cs:

    services.AddCookieAuthentication(options =>
    {
        options.LoginPath = "/Login";
    });
  2. Add the authentication service to the ConfigureServies method of Startup.cs:

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    });
  3. The above code will use a configuration option to get the user name.

Ensure dotnet user-secret works

  1. Navigate to the FrontEnd project folder and run dotnet 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.
    

Add Google and Twitter Authentication

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.

  1. Add the twitter authentication services to the ConfigureServies method of Startup.cs:

    var twitterConfig = Configuration.GetSection("twitter");
    if (twitterConfig["consumerKey"] != null)
    {
        services.AddTwitterAuthentication(options => twitterConfig.Bind(options));
    }
  2. 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}
    

image

Note: This section required you to have an app configured with Twitter. You can configure a Twitter app at https://console.developers.google.com/projectselector/apis/library. For more information, see this tutorial.

  1. Add the google authentication services to the ConfigureServices() method:

    var googleConfig = Configuration.GetSection("google");
    if (googleConfig["clientID"] != null)
    {
        services.AddGoogleAuthentication(options => googleConfig.Bind(options));
    }
  2. 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}
    

image

Add the Authentication middleware

  1. Add app.UseAuthentication() before app.UseMvc() in Startup.cs.

    app.UseAuthentication();
    
    app.UseMvc();

Add the Login page

  1. Add a Razor Page Login.cshtml and a page model Login.cshtml.cs to the Pages folder.

  2. In the Login.cshtml.cs get the IAuthenticationSchemeProvider add pass the registered authentication schemes to the page:

    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()
            {
                AuthSchemes = await _authSchemeProvider.GetRequestHandlerSchemesAsync();
    
                return Page();
            }
        }
    }
  3. 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>

Do external authentication

  1. 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);
    }
  2. The above logic will challenge with the approriate authentication scheme and will redirect to the "/Index" page on success.

Add the login/logout links

  1. 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 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 Oslo 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>
  2. Add a Controllers folder to the FrontEnd project, then and an AccountController.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("/");
            }
        }
    }

Adding admin section

Add an admin policy

  1. Add authorization service with an admin policy to the ConfigureServices() method of Startup.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"]);
        });
    });
  2. Add an admin username to appSettings.json, setting it to your Twitter or Google username:

    {
      "ServiceUrl": "http://localhost:56009/",
      "Admin": "<username>",
      "Logging": {
        "IncludeScopes": false,
        "Debug": {
          "LogLevel": {
            "Default": "Warning"
          }
        },
        "Console": {
          "LogLevel": {
            "Default": "Warning"
          }
        }
      }
    }   
  3. Add Microsoft.AspNetCore.Authorization to the list of usings in Index.cshtml.cs, then use the IAuthorizationService 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)
      {
          IsAdmin = await _authzService.AuthorizeAsync(User, "Admin");
    
          // More stuff here
          // ...
      }
  4. On the Index razor page, add an edit link to allow admins to edit sessions. You'll add the follosing code directly after the <p> tag that contains the session foreach loop:

    @if (Model.IsAdmin)
    {
       <p>
          <a asp-page="/Admin/EditSession" asp-route-id="@session.ID" class="btn btn-default btn-xs">Edit</a>
       </p>
    }
  5. Add a nested Admin folder to the Pages folder then add an EditSession.cshtml razor page and EditSession.cshtml.cs page model to it.

  6. Next, we'll protect this EditSession page it with an Admin policy by making the following change to the services.AddMvc() call in Startup.ConfigureServices:

    services.AddMvc()
            .AddRazorPagesOptions(options =>
            {
               options.AuthorizeFolder("/admin", "Admin");
            });

Add edit session form

  1. 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
          };
       }
    }
  2. Add the "{id}" route to the EditSession.cshtml form:

    @page "{id:int}"
    @model EditSessionModel
  3. 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")
    }
  4. Add code to handle the Save and Delete button actions in EditSession.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();
    }
  5. Add a [BindProperty] attribute to the Session property in EditSession.cshtml.cs to make sure properties get bound on form posts:

    [BindProperty]
    public Session Session { get; set; }
  6. The form should be fully functional.

Add success message to form post and use the PRG pattern

  1. Add a Message property and a ShowMessage property to EditSession.cshtml.cs:

    [TempData]
    public string Message { get; set; }
    
    public bool ShowMessage => !string.IsNullOrEmpty(Message);
  2. Set a success message in the OnPostAsync and OnPostDeleteAsync methods and change Page() to RedirectToPage():

    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");
    }
  3. 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">&times;</span>   </button>
            @Model.Message
        </div>
    }

Add auth tag helper

We're currently using if block to determine whether to show the login form in the header. We can clean up this code by using a Tag Helper.

  1. 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 Helper AuthzTagHelper.cs.
  2. Modify the HtmlTargetElement attribute to bind to all elements with an "authz" attribute:
    [HtmlTargetElement("*", Attributes = "authz")]
  3. Add an additional HtmlTargetElement attribute to bind to all elements with an "authz-policy" attribute:
    [HtmlTargetElement("*", Attributes = "authz-policy")]
  4. Inject the AuthorizationService as shown:
    private readonly IAuthorizationService _authzService;
    
    public AuthzTagHelper(IAuthorizationService authzService)
    {
        _authzService = authzService;
    }
  5. Add the following properties which will represent the auth and authz attributes we're binding to:
    [HtmlAttributeName("authz")]
    public bool RequiresAuthentication { get; set; }
    
    [HtmlAttributeName("authz-policy")]
    public string RequiredPolicy { get; set; } 
  6. Add a ViewContext property:
    [ViewContext]
    public ViewContext ViewContext { get; set; }
  7. Mark the ProcessAsync method as async.
  8. 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
            {
                authorized = await _authzService.AuthorizeAsync(ViewContext.HttpContext.User, RequiredPolicy);
                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();
        }
    }   
  9. We can now update the _Layout.cshtml method to replace the if block with declarative code using our new Tag Helper. Replace for <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>