From 9b17de4f0e1df721100084f83b56771a7eb6b119 Mon Sep 17 00:00:00 2001 From: Jason Watmore Date: Mon, 13 Aug 2018 20:50:25 +1000 Subject: [PATCH] initial commit --- .gitignore | 46 ++++++++++++++++++++ .vscode/launch.json | 32 ++++++++++++++ .vscode/tasks.json | 16 +++++++ Controllers/UsersController.cs | 39 +++++++++++++++++ Entities/User.cs | 12 ++++++ Helpers/AppSettings.cs | 7 ++++ LICENSE | 21 ++++++++++ Program.cs | 20 +++++++++ README.md | 3 ++ Services/UserService.cs | 73 ++++++++++++++++++++++++++++++++ Startup.cs | 76 ++++++++++++++++++++++++++++++++++ WebApi.csproj | 8 ++++ appsettings.Development.json | 10 +++++ appsettings.json | 11 +++++ 14 files changed, 374 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 Controllers/UsersController.cs create mode 100644 Entities/User.cs create mode 100644 Helpers/AppSettings.cs create mode 100644 LICENSE create mode 100644 Program.cs create mode 100644 README.md create mode 100644 Services/UserService.cs create mode 100644 Startup.cs create mode 100644 WebApi.csproj create mode 100644 appsettings.Development.json create mode 100644 appsettings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0e6cee --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages +typings + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Generated files +client/app/**/*.js +client/app/**/*.js.map + +# .NET compiled files +bin +obj \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..7d5f87e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/WebApi.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..cb5ab8d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + "version": "0.1.0", + "command": "dotnet", + "isShellCommand": true, + "args": [], + "tasks": [ + { + "taskName": "build", + "args": [ + "${workspaceRoot}/WebApi.csproj" + ], + "isBuildCommand": true, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Controllers/UsersController.cs b/Controllers/UsersController.cs new file mode 100644 index 0000000..0b552ec --- /dev/null +++ b/Controllers/UsersController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using WebApi.Services; +using WebApi.Entities; + +namespace WebApi.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + public class UsersController : ControllerBase + { + private IUserService _userService; + + public UsersController(IUserService userService) + { + _userService = userService; + } + + [AllowAnonymous] + [HttpPost("authenticate")] + public IActionResult Authenticate([FromBody]User userParam) + { + var user = _userService.Authenticate(userParam.Username, userParam.Password); + + if (user == null) + return BadRequest(new { message = "Username or password is incorrect" }); + + return Ok(user); + } + + [HttpGet] + public IActionResult GetAll() + { + var users = _userService.GetAll(); + return Ok(users); + } + } +} diff --git a/Entities/User.cs b/Entities/User.cs new file mode 100644 index 0000000..c3f4c88 --- /dev/null +++ b/Entities/User.cs @@ -0,0 +1,12 @@ +namespace WebApi.Entities +{ + public class User + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Token { get; set; } + } +} \ No newline at end of file diff --git a/Helpers/AppSettings.cs b/Helpers/AppSettings.cs new file mode 100644 index 0000000..b2df258 --- /dev/null +++ b/Helpers/AppSettings.cs @@ -0,0 +1,7 @@ +namespace WebApi.Helpers +{ + public class AppSettings + { + public string Secret { get; set; } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08605ec --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Jason Watmore + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..a976728 --- /dev/null +++ b/Program.cs @@ -0,0 +1,20 @@ +using System.IO; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace WebApi +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .UseUrls("http://localhost:4000") + .Build(); + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5bd1460 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# aspnet-core-jwt-authentication-api + +ASP.NET Core 2.1 - JWT Authentication API diff --git a/Services/UserService.cs b/Services/UserService.cs new file mode 100644 index 0000000..af14395 --- /dev/null +++ b/Services/UserService.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using WebApi.Entities; +using WebApi.Helpers; + +namespace WebApi.Services +{ + public interface IUserService + { + User Authenticate(string username, string password); + IEnumerable GetAll(); + } + + public class UserService : IUserService + { + // users hardcoded for simplicity, store in a db with hashed passwords in production applications + private List _users = new List + { + new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" } + }; + + private readonly AppSettings _appSettings; + + public UserService(IOptions appSettings) + { + _appSettings = appSettings.Value; + } + + public User Authenticate(string username, string password) + { + var user = _users.SingleOrDefault(x => x.Username == username && x.Password == password); + + // return null if user not found + if (user == null) + return null; + + // authentication successful so generate jwt token + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_appSettings.Secret); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new Claim[] + { + new Claim(ClaimTypes.Name, user.Id.ToString()) + }), + Expires = DateTime.UtcNow.AddDays(7), + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + var token = tokenHandler.CreateToken(tokenDescriptor); + user.Token = tokenHandler.WriteToken(token); + + // remove password before returning + user.Password = null; + + return user; + } + + public IEnumerable GetAll() + { + // return users without passwords + return _users.Select(x => { + x.Password = null; + return x; + }); + } + } +} \ No newline at end of file diff --git a/Startup.cs b/Startup.cs new file mode 100644 index 0000000..3843bcc --- /dev/null +++ b/Startup.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using WebApi.Helpers; +using WebApi.Services; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace WebApi +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddCors(); + services.AddMvc(); + + // configure strongly typed settings objects + var appSettingsSection = Configuration.GetSection("AppSettings"); + services.Configure(appSettingsSection); + + // configure jwt authentication + var appSettings = appSettingsSection.Get(); + var key = Encoding.ASCII.GetBytes(appSettings.Secret); + services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(x => + { + x.RequireHttpsMetadata = false; + x.SaveToken = true; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false + }; + }); + + // configure DI for application services + services.AddScoped(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + // global cors policy + app.UseCors(x => x + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + + app.UseAuthentication(); + + app.UseMvc(); + } + } +} diff --git a/WebApi.csproj b/WebApi.csproj new file mode 100644 index 0000000..3046078 --- /dev/null +++ b/WebApi.csproj @@ -0,0 +1,8 @@ + + + netcoreapp2.1 + + + + + \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..fa8ce71 --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..9c74b39 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,11 @@ +{ + "AppSettings": { + "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING" + }, + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +}