| | | 1 | | using Microsoft.AspNetCore.Authentication; |
| | | 2 | | using Microsoft.AspNetCore.Authentication.Cookies; |
| | | 3 | | using Microsoft.AspNetCore.Authorization; |
| | | 4 | | using Microsoft.AspNetCore.Mvc; |
| | | 5 | | using ProjectTemplate.Web.Models; |
| | | 6 | | |
| | | 7 | | namespace ProjectTemplate.Web.Controllers; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Provides actions for user authentication, including login, logout, and access denied handling. |
| | | 11 | | /// </summary> |
| | | 12 | | /// <remarks>This controller exposes endpoints for user authentication workflows. It supports external |
| | | 13 | | /// authentication providers and enforces security best practices such as requiring local return URLs to prevent open |
| | | 14 | | /// redirect vulnerabilities. Actions are decorated with appropriate authorization and anti-forgery attributes as |
| | | 15 | | /// needed.</remarks> |
| | | 16 | | /// <param name="schemeProvider">The authentication scheme provider used to retrieve available external authentication s |
| | | 17 | | /// Cannot be null.</param> |
| | 8 | 18 | | public class AccountController(IAuthenticationSchemeProvider schemeProvider) : Controller |
| | | 19 | | { |
| | 8 | 20 | | private readonly IAuthenticationSchemeProvider _schemeProvider = schemeProvider; |
| | | 21 | | |
| | | 22 | | /// <summary> |
| | | 23 | | /// Displays the login page and provides available external authentication providers. |
| | | 24 | | /// </summary> |
| | | 25 | | /// <remarks>This action is accessible without authentication. Only local return URLs are permitted to |
| | | 26 | | /// prevent open redirect vulnerabilities.</remarks> |
| | | 27 | | /// <param name="returnUrl">The URL to redirect to after a successful login. If null or empty, the user is redirecte |
| | | 28 | | /// root. Must be a local URL.</param> |
| | | 29 | | /// <returns>A view result that renders the login page with available external authentication providers, or a bad re |
| | | 30 | | /// result if the return URL is not local.</returns> |
| | | 31 | | [HttpGet("/Account/Login")] |
| | | 32 | | [AllowAnonymous] |
| | | 33 | | public async Task<IActionResult> Login(string? returnUrl = null) |
| | | 34 | | { |
| | 4 | 35 | | string safeReturnUrl = string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl; |
| | | 36 | | |
| | 4 | 37 | | if (!Url.IsLocalUrl(safeReturnUrl)) |
| | | 38 | | { |
| | 2 | 39 | | return BadRequest(); |
| | | 40 | | } |
| | | 41 | | |
| | 2 | 42 | | IEnumerable<AuthenticationScheme> schemes = await _schemeProvider.GetAllSchemesAsync(); |
| | | 43 | | |
| | 2 | 44 | | AccountLoginViewModel model = new() |
| | 2 | 45 | | { |
| | 2 | 46 | | ReturnUrl = safeReturnUrl, |
| | 2 | 47 | | ExternalProviders = schemes |
| | 4 | 48 | | .Where(scheme => !string.Equals( |
| | 4 | 49 | | scheme.Name, |
| | 4 | 50 | | CookieAuthenticationDefaults.AuthenticationScheme, |
| | 4 | 51 | | StringComparison.Ordinal)) |
| | 2 | 52 | | .Where(scheme => !string.IsNullOrWhiteSpace(scheme.DisplayName)) |
| | 2 | 53 | | .Select(scheme => new ExternalAuthenticationProviderViewModel |
| | 2 | 54 | | { |
| | 2 | 55 | | Scheme = scheme.Name, |
| | 2 | 56 | | DisplayName = scheme.DisplayName ?? scheme.Name |
| | 2 | 57 | | }) |
| | 0 | 58 | | .OrderBy(provider => provider.DisplayName) |
| | 2 | 59 | | .ToList() |
| | 2 | 60 | | }; |
| | | 61 | | |
| | 2 | 62 | | return View(model); |
| | 4 | 63 | | } |
| | | 64 | | |
| | | 65 | | /// <summary> |
| | | 66 | | /// Signs out the current user and redirects to the specified return URL. |
| | | 67 | | /// </summary> |
| | | 68 | | /// <remarks>This action requires a valid anti-forgery token and only accepts local URLs for redirection |
| | | 69 | | /// to help prevent open redirect vulnerabilities.</remarks> |
| | | 70 | | /// <param name="returnUrl">The URL to redirect to after sign-out. If null or empty, defaults to the application's r |
| | | 71 | | /// local URL.</param> |
| | | 72 | | /// <returns>A redirect result to the specified local return URL if sign-out is successful; otherwise, a bad request |
| | | 73 | | /// if the return URL is not local.</returns> |
| | | 74 | | [HttpPost("/Account/Logout")] |
| | | 75 | | [ValidateAntiForgeryToken] |
| | | 76 | | public async Task<IActionResult> Logout(string? returnUrl = null) |
| | | 77 | | { |
| | 2 | 78 | | string safeReturnUrl = string.IsNullOrWhiteSpace(returnUrl) ? "/" : returnUrl; |
| | | 79 | | |
| | 2 | 80 | | if (!Url.IsLocalUrl(safeReturnUrl)) |
| | | 81 | | { |
| | 0 | 82 | | return BadRequest(); |
| | | 83 | | } |
| | | 84 | | |
| | 2 | 85 | | await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); |
| | | 86 | | |
| | 2 | 87 | | return LocalRedirect(safeReturnUrl); |
| | 2 | 88 | | } |
| | | 89 | | |
| | | 90 | | /// <summary> |
| | | 91 | | /// Handles requests to the access denied page and returns a view indicating that the user does not have permission |
| | | 92 | | /// to access the requested resource. |
| | | 93 | | /// </summary> |
| | | 94 | | /// <remarks>This action is accessible to all users, including unauthenticated users. The response status |
| | | 95 | | /// code is set to 403 to indicate forbidden access.</remarks> |
| | | 96 | | /// <returns>A view result that displays the access denied page with a 403 Forbidden status code.</returns> |
| | | 97 | | [HttpGet("/Account/AccessDenied")] |
| | | 98 | | [AllowAnonymous] |
| | | 99 | | public IActionResult AccessDenied() |
| | | 100 | | { |
| | 2 | 101 | | Response.StatusCode = StatusCodes.Status403Forbidden; |
| | 2 | 102 | | return View(); |
| | | 103 | | } |
| | | 104 | | } |