├── omnisharp.json
├── WebApi.csproj
├── appsettings.Development.json
├── README.md
├── appsettings.json
├── Models
└── AuthenticateModel.cs
├── Entities
└── User.cs
├── .vscode
├── tasks.json
└── launch.json
├── Program.cs
├── .gitignore
├── LICENSE
├── Controllers
└── UsersController.cs
├── Services
└── UserService.cs
├── Startup.cs
└── Helpers
└── BasicAuthenticationHandler.cs
/omnisharp.json:
--------------------------------------------------------------------------------
1 | {
2 | "msbuild": {
3 | "useBundledOnly": true
4 | }
5 | }
--------------------------------------------------------------------------------
/WebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net5.0
4 |
5 |
--------------------------------------------------------------------------------
/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dotnet-5-basic-authentication-api
2 |
3 | .NET 5.0 - Basic HTTP Authentication API
4 |
5 | For documentation and instructions check out https://jasonwatmore.com/post/2021/05/19/net-5-basic-authentication-tutorial-with-example-api
--------------------------------------------------------------------------------
/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*"
10 | }
11 |
--------------------------------------------------------------------------------
/Models/AuthenticateModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace WebApi.Models
4 | {
5 | public class AuthenticateModel
6 | {
7 | [Required]
8 | public string Username { get; set; }
9 |
10 | [Required]
11 | public string Password { get; set; }
12 | }
13 | }
--------------------------------------------------------------------------------
/Entities/User.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace WebApi.Entities
4 | {
5 | public class User
6 | {
7 | public int Id { get; set; }
8 | public string FirstName { get; set; }
9 | public string LastName { get; set; }
10 | public string Username { get; set; }
11 |
12 | [JsonIgnore]
13 | public string Password { get; set; }
14 | }
15 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/WebApi.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Hosting;
2 | using Microsoft.Extensions.Hosting;
3 |
4 | namespace WebApi
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IHostBuilder CreateHostBuilder(string[] args) =>
14 | Host.CreateDefaultBuilder(args)
15 | .ConfigureWebHostDefaults(webBuilder =>
16 | {
17 | webBuilder.UseStartup()
18 | .UseUrls("http://localhost:4000");
19 | });
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 | typings
33 |
34 | # Optional npm cache directory
35 | .npm
36 |
37 | # Optional REPL history
38 | .node_repl_history
39 |
40 | # .NET compiled files
41 | bin
42 | obj
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021 Jason Watmore
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Controllers/UsersController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.AspNetCore.Authorization;
3 | using WebApi.Services;
4 | using System.Threading.Tasks;
5 | using WebApi.Models;
6 |
7 | namespace WebApi.Controllers
8 | {
9 | [Authorize]
10 | [ApiController]
11 | [Route("[controller]")]
12 | public class UsersController : ControllerBase
13 | {
14 | private IUserService _userService;
15 |
16 | public UsersController(IUserService userService)
17 | {
18 | _userService = userService;
19 | }
20 |
21 | [AllowAnonymous]
22 | [HttpPost("authenticate")]
23 | public async Task Authenticate([FromBody]AuthenticateModel model)
24 | {
25 | var user = await _userService.Authenticate(model.Username, model.Password);
26 |
27 | if (user == null)
28 | return BadRequest(new { message = "Username or password is incorrect" });
29 |
30 | return Ok(user);
31 | }
32 |
33 | [HttpGet]
34 | public async Task GetAll()
35 | {
36 | var users = await _userService.GetAll();
37 | return Ok(users);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (web)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | // If you have changed target frameworks, make sure to update the program path.
13 | "program": "${workspaceFolder}/bin/Debug/net5.0/WebApi.dll",
14 | "args": [],
15 | "cwd": "${workspaceFolder}",
16 | "stopAtEntry": false,
17 | "internalConsoleOptions": "openOnSessionStart",
18 | "env": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | },
21 | "sourceFileMap": {
22 | "/Views": "${workspaceFolder}/Views"
23 | }
24 | },
25 | {
26 | "name": ".NET Core Attach",
27 | "type": "coreclr",
28 | "request": "attach",
29 | "processId": "${command:pickProcess}"
30 | }
31 | ]
32 | }
--------------------------------------------------------------------------------
/Services/UserService.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using WebApi.Entities;
5 |
6 | namespace WebApi.Services
7 | {
8 | public interface IUserService
9 | {
10 | Task Authenticate(string username, string password);
11 | Task> GetAll();
12 | }
13 |
14 | public class UserService : IUserService
15 | {
16 | // users hardcoded for simplicity, store in a db with hashed passwords in production applications
17 | private List _users = new List
18 | {
19 | new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" }
20 | };
21 |
22 | public async Task Authenticate(string username, string password)
23 | {
24 | // wrapped in "await Task.Run" to mimic fetching user from a db
25 | var user = await Task.Run(() => _users.SingleOrDefault(x => x.Username == username && x.Password == password));
26 |
27 | // return null if user not found
28 | if (user == null)
29 | return null;
30 |
31 | // authentication successful so return user details
32 | return user;
33 | }
34 |
35 | public async Task> GetAll()
36 | {
37 | // wrapped in "await Task.Run" to mimic fetching users from a db
38 | return await Task.Run(() => _users);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using WebApi.Helpers;
6 | using WebApi.Services;
7 | using Microsoft.AspNetCore.Authentication;
8 |
9 | namespace WebApi
10 | {
11 | public class Startup
12 | {
13 | public Startup(IConfiguration configuration)
14 | {
15 | Configuration = configuration;
16 | }
17 |
18 | public IConfiguration Configuration { get; }
19 |
20 | // This method gets called by the runtime. Use this method to add services to the container.
21 | public void ConfigureServices(IServiceCollection services)
22 | {
23 | services.AddCors();
24 | services.AddControllers();
25 |
26 | // configure basic authentication
27 | services.AddAuthentication("BasicAuthentication")
28 | .AddScheme("BasicAuthentication", null);
29 |
30 | // configure DI for application services
31 | services.AddScoped();
32 | }
33 |
34 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
35 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
36 | {
37 | app.UseRouting();
38 |
39 | // global cors policy
40 | app.UseCors(x => x
41 | .AllowAnyOrigin()
42 | .AllowAnyMethod()
43 | .AllowAnyHeader());
44 |
45 | app.UseAuthentication();
46 | app.UseAuthorization();
47 |
48 | app.UseEndpoints(endpoints => endpoints.MapControllers());
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Helpers/BasicAuthenticationHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http.Headers;
3 | using System.Security.Claims;
4 | using System.Text;
5 | using System.Text.Encodings.Web;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Authentication;
8 | using Microsoft.AspNetCore.Authorization;
9 | using Microsoft.AspNetCore.Http;
10 | using Microsoft.Extensions.Logging;
11 | using Microsoft.Extensions.Options;
12 | using WebApi.Entities;
13 | using WebApi.Services;
14 |
15 | namespace WebApi.Helpers
16 | {
17 | public class BasicAuthenticationHandler : AuthenticationHandler
18 | {
19 | private readonly IUserService _userService;
20 |
21 | public BasicAuthenticationHandler(
22 | IOptionsMonitor options,
23 | ILoggerFactory logger,
24 | UrlEncoder encoder,
25 | ISystemClock clock,
26 | IUserService userService)
27 | : base(options, logger, encoder, clock)
28 | {
29 | _userService = userService;
30 | }
31 |
32 | protected override async Task HandleAuthenticateAsync()
33 | {
34 | // skip authentication if endpoint has [AllowAnonymous] attribute
35 | var endpoint = Context.GetEndpoint();
36 | if (endpoint?.Metadata?.GetMetadata() != null)
37 | return AuthenticateResult.NoResult();
38 |
39 | if (!Request.Headers.ContainsKey("Authorization"))
40 | return AuthenticateResult.Fail("Missing Authorization Header");
41 |
42 | User user = null;
43 | try
44 | {
45 | var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
46 | var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
47 | var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
48 | var username = credentials[0];
49 | var password = credentials[1];
50 | user = await _userService.Authenticate(username, password);
51 | }
52 | catch
53 | {
54 | return AuthenticateResult.Fail("Invalid Authorization Header");
55 | }
56 |
57 | if (user == null)
58 | return AuthenticateResult.Fail("Invalid Username or Password");
59 |
60 | var claims = new[] {
61 | new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
62 | new Claim(ClaimTypes.Name, user.Username),
63 | };
64 | var identity = new ClaimsIdentity(claims, Scheme.Name);
65 | var principal = new ClaimsPrincipal(identity);
66 | var ticket = new AuthenticationTicket(principal, Scheme.Name);
67 |
68 | return AuthenticateResult.Success(ticket);
69 | }
70 |
71 | protected override Task HandleChallengeAsync(AuthenticationProperties properties)
72 | {
73 | Response.Headers["WWW-Authenticate"] = "Basic realm=\"\", charset=\"UTF-8\"";
74 | return base.HandleChallengeAsync(properties);
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------