├── blazor.jwttest.Client ├── Pages │ ├── _ViewImports.cshtml │ ├── error403.cshtml │ ├── error401.cshtml │ ├── error500.cshtml │ ├── Index.cshtml │ ├── AddTodo.cshtml │ ├── EditTodo.cshtml │ ├── AddUser.cshtml │ ├── EditUser.cshtml │ ├── Login.cshtml │ ├── EditUsers.cshtml │ ├── DeleteTodo.cshtml │ ├── DeleteUser.cshtml │ └── EditTodos.cshtml ├── wwwroot │ ├── css │ │ ├── open-iconic │ │ │ ├── font │ │ │ │ ├── fonts │ │ │ │ │ ├── open-iconic.eot │ │ │ │ │ ├── open-iconic.otf │ │ │ │ │ ├── open-iconic.ttf │ │ │ │ │ ├── open-iconic.woff │ │ │ │ │ └── open-iconic.svg │ │ │ │ └── css │ │ │ │ │ └── open-iconic-bootstrap.min.css │ │ │ ├── ICON-LICENSE │ │ │ ├── README.md │ │ │ └── FONT-LICENSE │ │ └── site.css │ └── index.html ├── App.cshtml ├── Classes │ ├── Enumerations.cs │ ├── Extensions.cs │ ├── AuthenticationDelegationHandler.cs │ ├── JwtDecode.cs │ └── ApplicationState.cs ├── _ViewImports.cshtml ├── Exceptions │ ├── HttpUnauthorizedException.cs │ ├── HttpForbiddenException.cs │ └── HttpInternalServerErrorException.cs ├── Program.cs ├── Linker.xml ├── Shared │ ├── MainLayout.cshtml │ ├── PopupDialog.cshtml │ ├── RolesEditor.cshtml │ ├── SlideSwitch.cshtml │ ├── NavMenu.cshtml │ ├── TodoForm.cshtml │ └── UserForm.cshtml ├── Startup.cs └── blazor.jwttest.Client.csproj ├── blazor.jwttest.Shared ├── IViewModel.cs ├── JwToken.cs ├── LoginDetails.cs ├── blazor.jwttest.Shared.csproj ├── Todo.cs └── User.cs ├── blazor.jwttest.Server ├── Services │ ├── Exceptions │ │ ├── ValidationEnums.cs │ │ ├── EntityCreationNotAllowed.cs │ │ ├── NoEntityFoundForPredicateException.cs │ │ ├── EntityNotFoundException.cs │ │ └── UserNotValidatedException.cs │ ├── Todos.cs │ ├── Users.cs │ └── GenericDbAccess.cs ├── appsettings.json ├── Database │ ├── Entities │ │ ├── DbEntityBase.cs │ │ ├── DbTodo.cs │ │ └── DbUser.cs │ └── EfDataContext.cs ├── Program.cs ├── blazor.jwttest.Server.csproj ├── Controllers │ ├── UsersController.cs │ ├── TodoController.cs │ └── AuthenticationController.cs └── Startup.cs ├── LICENSE ├── README.md ├── blazor.jwttest.sln └── .gitignore /blazor.jwttest.Client/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @layout MainLayout 2 | -------------------------------------------------------------------------------- /blazor.jwttest.Shared/IViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace blazor.jwttest.Shared 2 | { 3 | public interface IViewModel 4 | { 5 | int Id { get; set; } 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /blazor.jwttest.Shared/JwToken.cs: -------------------------------------------------------------------------------- 1 | namespace blazor.jwttest.Shared 2 | { 3 | public class JwToken 4 | { 5 | public string Token { get; set; } 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawty/blazor.jwttest/HEAD/blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawty/blazor.jwttest/HEAD/blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawty/blazor.jwttest/HEAD/blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shawty/blazor.jwttest/HEAD/blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /blazor.jwttest.Shared/LoginDetails.cs: -------------------------------------------------------------------------------- 1 | namespace blazor.jwttest.Shared 2 | { 3 | public class LoginDetails 4 | { 5 | public string Username { get; set; } 6 | public string Password { get; set; } 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/App.cshtml: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /blazor.jwttest.Shared/blazor.jwttest.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 7.3 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Classes/Enumerations.cs: -------------------------------------------------------------------------------- 1 | namespace blazor.jwttest.Client.Classes 2 | { 3 | public enum NavigationFailReason 4 | { 5 | Unknown = -1, 6 | NoFail = 0, 7 | NotLoggedIn = 1, 8 | RoleNotAllowed 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Blazor.Layouts 3 | @using Microsoft.AspNetCore.Blazor.Routing 4 | @using Microsoft.JSInterop 5 | @using blazor.jwttest.Client 6 | @using blazor.jwttest.Client.Shared 7 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/Exceptions/ValidationEnums.cs: -------------------------------------------------------------------------------- 1 | namespace blazor.jwttest.Server.Services.Exceptions 2 | { 3 | public enum ValidationFailReason 4 | { 5 | Unknown = 0, 6 | UserNotFound, 7 | PasswordDoesNotMatch, 8 | DatabaseError 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /blazor.jwttest.Shared/Todo.cs: -------------------------------------------------------------------------------- 1 | namespace blazor.jwttest.Shared 2 | { 3 | public class Todo : IViewModel 4 | { 5 | public int Id { get; set; } 6 | public string Title { get; set; } 7 | public string FullDescription { get; set; } 8 | public bool Done { get; set; } 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Exceptions/HttpUnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace blazor.jwttest.Client.Exceptions 4 | { 5 | public class HttpUnauthorizedException : HttpRequestException 6 | { 7 | public HttpUnauthorizedException() : base("401 (Unauthorized)") { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Exceptions/HttpForbiddenException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | 4 | namespace blazor.jwttest.Client.Exceptions 5 | { 6 | public class HttpForbiddenException : HttpRequestException 7 | { 8 | public HttpForbiddenException() : base("403 (Forbidden)") { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "PostgresDbConnection": "Server=;Port=5432;DataBase=;User Id=;Password=" 4 | }, 5 | "JwtSecurityKey": "IAMth3K3y2Y0U7secur1ty", 6 | "JwtIssuer": "http://jwttest.example.com", 7 | "JwtExpiryInMinutes": 30 8 | 9 | } -------------------------------------------------------------------------------- /blazor.jwttest.Client/Exceptions/HttpInternalServerErrorException.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace blazor.jwttest.Client.Exceptions 4 | { 5 | public class HttpInternalServerErrorException : HttpRequestException 6 | { 7 | public HttpInternalServerErrorException() : base("500 (Internal Server Error)") { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/Exceptions/EntityCreationNotAllowed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazor.jwttest.Server.Services.Exceptions 4 | { 5 | public class EntityCreationNotAllowed : Exception 6 | { 7 | public EntityCreationNotAllowed(Type entityType) 8 | : base($"Creation of entitys of type {entityType.Name} not allowed.") 9 | { } 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Classes/Extensions.cs: -------------------------------------------------------------------------------- 1 | namespace blazor.jwttest.Client.Classes 2 | { 3 | public static class Extensions 4 | { 5 | public static int GetNextHighestMultiple(this int source, int multipicand) 6 | { 7 | int result = source; 8 | while((result % multipicand) != 0) 9 | { 10 | result++; 11 | } 12 | return result; 13 | } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /blazor.jwttest.Shared/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazor.jwttest.Shared 4 | { 5 | public class User : IViewModel 6 | { 7 | public int Id { get; set; } 8 | 9 | public string LoginName { get; set; } 10 | 11 | public string FullName { get; set; } 12 | 13 | public string Email { get; set; } 14 | 15 | public string Password { get; set; } 16 | 17 | public string[] AllowedRoles { get; set; } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | blazor.jwttest 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Blazor.Hosting; 2 | 3 | namespace blazor.jwttest.Client 4 | { 5 | public class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | CreateHostBuilder(args).Build().Run(); 10 | } 11 | 12 | public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => 13 | BlazorWebAssemblyHost.CreateDefaultBuilder() 14 | .UseBlazorStartup(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Database/Entities/DbEntityBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace blazor.jwttest.Server.Database.Entities 6 | { 7 | public class DbEntityBase 8 | { 9 | [Column("id")] 10 | public int Id { get; set; } 11 | 12 | [Column("datecreated")] 13 | public DateTime DateCreated { get; set; } 14 | 15 | [Column("datemodified")] 16 | public DateTime? DateModified { get; set; } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/Exceptions/NoEntityFoundForPredicateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazor.jwttest.Server.Services.Exceptions 4 | { 5 | // This Exception is designed to be thrown when using a predicate to look for a single entity which is not found 6 | public class NoEntityFoundForPredicateException : Exception 7 | { 8 | public Guid RecordId { get; set; } 9 | 10 | public NoEntityFoundForPredicateException() 11 | : base($"No entity was found matching the supplied predicate.") 12 | { } 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Database/Entities/DbTodo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace blazor.jwttest.Server.Database.Entities 4 | { 5 | [Table("todos", Schema = "public")] 6 | public class DbTodo : DbEntityBase 7 | { 8 | [Column("title")] 9 | public string Title { get; set; } 10 | 11 | [Column("fulldescription")] 12 | public string FullDescription { get; set; } 13 | 14 | [Column("email")] 15 | public string Email { get; set; } 16 | 17 | [Column("done")] 18 | public bool Done { get; set; } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazor.jwttest.Server.Services.Exceptions 4 | { 5 | // This exception is designed to be thrown when the specific entity ID is known, but not found. 6 | public class EntityNotFoundException : Exception 7 | { 8 | public int RecordId { get; set; } 9 | 10 | public EntityNotFoundException(int recordId, Type entityType) 11 | : base($"Requested entity of type {entityType.Name} with id {recordId.ToString()} was not found in database") 12 | { 13 | RecordId = recordId; 14 | } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Linker.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/Todos.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Server.Database; 2 | using blazor.jwttest.Server.Database.Entities; 3 | using blazor.jwttest.Shared; 4 | 5 | namespace blazor.jwttest.Server.Services 6 | { 7 | public class Todos : GenericDbAccess 8 | { 9 | public Todos(EfDataContext db) : base(db) 10 | { } 11 | 12 | public override void ApplyCustomMappings() 13 | { 14 | // If you need any custom mappings for mapster put them in here, and add "using mapster" to the usings at the top 15 | // See Users.cs for an example 16 | } 17 | 18 | // Custom object functions go here 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Database/Entities/DbUser.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace blazor.jwttest.Server.Database.Entities 4 | { 5 | [Table("users", Schema = "public")] 6 | public class DbUser : DbEntityBase 7 | { 8 | [Column("loginname")] 9 | public string LoginName { get; set; } 10 | 11 | [Column("fullname")] 12 | public string FullName { get; set; } 13 | 14 | [Column("email")] 15 | public string Email { get; set; } 16 | 17 | [Column("password")] 18 | public string Password { get; set; } 19 | 20 | [Column("allowedroles")] 21 | public string[] AllowedRoles { get; set; } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/error403.cshtml: -------------------------------------------------------------------------------- 1 | @page "/error403" 2 | 3 |
4 |
5 |
 
6 |
7 |
8 |
9 |

Forbidden (403)

10 |

Your current login/account does not allow you access the page or resource you requested.

11 |
12 |
13 |

If you believe that this is an error then you need to contact support to have the error resolved.

14 |
15 |
 
16 |
17 |
18 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace blazor.jwttest.Server 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | BuildWebHost(args).Run(); 12 | } 13 | 14 | public static IWebHost BuildWebHost(string[] args) => 15 | WebHost.CreateDefaultBuilder(args) 16 | .UseConfiguration(new ConfigurationBuilder() 17 | .AddCommandLine(args) 18 | .Build()) 19 | .UseStartup() 20 | .Build(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/error401.cshtml: -------------------------------------------------------------------------------- 1 | @page "/error401" 2 | 3 |
4 |
5 |
 
6 |
7 |
8 |
9 |

Unauthorized (401)

10 |

You are either not currently logged in, or your account is not authorized to access the requested resource.

11 |
12 |
13 |

If you believe that this is an error then you need to contact support to have the error resolved.

14 |
15 |
 
16 |
17 |
18 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/error500.cshtml: -------------------------------------------------------------------------------- 1 | @page "/error500" 2 | 3 |
4 |
5 |
 
6 |
7 |
8 |
9 |

Internal Server Error (500)

10 |

Your request to the server returned a server error, this may be temporary.

11 |
12 |
13 |

Please wait a while and retry your request, if you continue to see this error for the same thing please inform support.

14 |
15 |
 
16 |
17 |
18 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/Exceptions/UserNotValidatedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace blazor.jwttest.Server.Services.Exceptions 4 | { 5 | // This exception is designed to be thrown when a user login cannot be validated against the database. 6 | public class UserNotValidatedException : Exception 7 | { 8 | public ValidationFailReason ValidationFailReason { get; private set; } 9 | public string UserLoginName { get; private set; } 10 | 11 | public UserNotValidatedException(ValidationFailReason failReason, string userLoginName) 12 | : base($"User Validation Failed.") 13 | { 14 | UserLoginName = userLoginName; 15 | ValidationFailReason = failReason; 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Shared/MainLayout.cshtml: -------------------------------------------------------------------------------- 1 | @inherits BlazorLayoutComponent 2 | @using blazor.jwttest.Client.Classes 3 | @inject ApplicationState AppState 4 | 5 | 8 | 9 |
10 |
11 | @if (AppState.IsLoggedIn) 12 | { 13 |

Logged in as @AppState.UserName  Logout

14 | } 15 | else 16 | { 17 |

Login

18 | } 19 |
20 | 21 |
22 | @Body 23 |
24 |
25 | 26 | @functions { 27 | 28 | protected async Task Logout() 29 | { 30 | await AppState.Logout(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Startup.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Client.Classes; 2 | using Blazor.Extensions.Storage; 3 | using Microsoft.AspNetCore.Blazor.Browser.Services; 4 | using Microsoft.AspNetCore.Blazor.Builder; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using System; 7 | using System.Net.Http; 8 | 9 | namespace blazor.jwttest.Client 10 | { 11 | public class Startup 12 | { 13 | public void ConfigureServices(IServiceCollection services) 14 | { 15 | services.AddStorage(); 16 | 17 | services.AddSingleton(); 18 | services.AddTransient(); 19 | 20 | services.AddSingleton(x => new HttpClient(new AuthenticationDelegationHandler()) 21 | { 22 | BaseAddress = new Uri(BrowserUriHelper.Instance.GetBaseUri()) 23 | }); 24 | 25 | } 26 | 27 | public void Configure(IBlazorApplicationBuilder app) 28 | { 29 | app.AddComponent("app"); 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/blazor.jwttest.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 7.3 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Shared/PopupDialog.cshtml: -------------------------------------------------------------------------------- 1 | 33 |
34 |
35 |
36 | @ChildContent 37 |
38 |
39 |
40 | 41 | 42 | @functions { 43 | 44 | [Parameter] 45 | private bool Show { get; set; } 46 | 47 | [Parameter] 48 | private RenderFragment ChildContent { get; set; } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Peter "Shawty" Shaw 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 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using blazor.jwttest.Shared 2 | @using System.Threading; 3 | @page "/" 4 | @inject HttpClient Http 5 | 6 |

Todo's List

7 | 8 | @if (todos == null) 9 | { 10 |

Loading list...

11 | } 12 | else 13 | { 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | @foreach (var todo in todos) 24 | { 25 | 26 | 27 | 28 | 29 | 30 | } 31 | 32 |
#TitleDone
@todo.Id@todo.Title@todo.Done
33 | 34 | } 35 | 36 | @functions { 37 | 38 | Todo[] todos; 39 | 40 | private bool showDlg { get; set; } = false; 41 | private Timer _timer; 42 | 43 | protected override async Task OnInitAsync() 44 | { 45 | todos = await Http.GetJsonAsync("api/todos/all"); 46 | } 47 | 48 | protected void clickHandler() 49 | { 50 | showDlg = true; 51 | _timer = new Timer(TimerCallBack, null, 5000, Timeout.Infinite); 52 | 53 | } 54 | 55 | void TimerCallBack(object state) 56 | { 57 | showDlg = false; 58 | StateHasChanged(); 59 | } 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Database/EfDataContext.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Server.Database.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace blazor.jwttest.Server.Database 5 | { 6 | public class EfDataContext : DbContext 7 | { 8 | public DbSet Users { get; set; } 9 | public DbSet Todos { get; set; } 10 | 11 | public EfDataContext() : base() { } 12 | 13 | public EfDataContext(DbContextOptions options) : base(options) 14 | { } 15 | 16 | protected override void OnModelCreating(ModelBuilder builder) 17 | { 18 | // Make sure the base is called 19 | base.OnModelCreating(builder); 20 | 21 | // Call our data seeder 22 | SeedData(builder); 23 | 24 | } 25 | 26 | private void SeedData(ModelBuilder builder) 27 | { 28 | // NOTE: We use ".HasData" here, this means this data will ONLY be seeded IF the table 29 | // is completley empty. 30 | builder.Entity() 31 | .HasData( 32 | new DbUser 33 | { 34 | Id = 1, 35 | LoginName = "admin", 36 | FullName = "Administrator", 37 | Email = "admin@mycorp.com", 38 | Password = BCrypt.Net.BCrypt.HashPassword("letmein"), 39 | AllowedRoles = new string[] {"admin", "useredit", "roleedit"} 40 | } 41 | ); 42 | 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Classes/AuthenticationDelegationHandler.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Client.Exceptions; 2 | using Microsoft.AspNetCore.Blazor.Browser.Http; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace blazor.jwttest.Client.Classes 9 | { 10 | public class AuthenticationDelegationHandler : DelegatingHandler 11 | { 12 | // This handler overrides all HttpClient calls to the backend server, allowing us to better 13 | // handle different error codes in our pages and components. Credit goes to 14 | // @kswoll (Kirk Woll) in the Blazor gitter group for pointing the way. 15 | public AuthenticationDelegationHandler() : base (new BrowserHttpMessageHandler()) 16 | { } 17 | 18 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 19 | { 20 | var response = await base.SendAsync(request, cancellationToken); 21 | 22 | if (response.StatusCode == HttpStatusCode.Unauthorized) 23 | { 24 | throw new HttpUnauthorizedException(); 25 | } 26 | 27 | if (response.StatusCode == HttpStatusCode.Forbidden) 28 | { 29 | throw new HttpForbiddenException(); 30 | } 31 | 32 | if (response.StatusCode == HttpStatusCode.InternalServerError) 33 | { 34 | throw new HttpInternalServerErrorException(); 35 | } 36 | 37 | return response; 38 | 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Shared/RolesEditor.cshtml: -------------------------------------------------------------------------------- 1 | @using blazor.jwttest.Client.Classes 2 | @inject ApplicationState AppState 3 | 4 |
5 |
    6 |
  • 7 |
    8 | 9 |
    10 | 11 |
    12 |
    13 |
  • 14 | 15 | @foreach (string role in RolesList) 16 | { 17 |
  • 18 | @role 19 |
  • 20 | } 21 | 22 |
23 |
24 | 25 | 26 | @functions { 27 | 28 | [Parameter] 29 | private string[] Roles { get; set; } 30 | 31 | public List RolesList { get; set; } 32 | private string newRoleName; 33 | 34 | protected override void OnParametersSet() 35 | { 36 | if(RolesList == null) 37 | { 38 | RolesList = new List(); 39 | if (Roles != null) 40 | { 41 | RolesList.AddRange(Roles); 42 | } 43 | } 44 | 45 | } 46 | 47 | private void handleDeleteClick(string role) 48 | { 49 | RolesList.Remove(role); 50 | } 51 | 52 | private void handleAddClick() 53 | { 54 | if (!String.IsNullOrEmpty(newRoleName)) 55 | { 56 | RolesList.Add(newRoleName); 57 | newRoleName = String.Empty; 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/AddTodo.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/add/todo" 11 | 12 | 13 | 14 | @functions { 15 | 16 | private Todo newTodo; 17 | TodoForm todoForm; 18 | 19 | private readonly List allowedRoles = new List() { "todoedit" }; 20 | NavigationFailReason failReason = NavigationFailReason.Unknown; 21 | 22 | protected override void OnInit() 23 | { 24 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 25 | { 26 | if (failReason == NavigationFailReason.RoleNotAllowed) 27 | { 28 | UriHelper.NavigateTo("/error403"); 29 | return; 30 | } 31 | return; 32 | } 33 | 34 | newTodo = new Todo(); 35 | } 36 | 37 | private async void SaveClicked() 38 | { 39 | try 40 | { 41 | await Http.SendJsonAsync(HttpMethod.Post, "api/todos/create", todoForm.TodoData); 42 | } 43 | catch (HttpUnauthorizedException) 44 | { 45 | UriHelper.NavigateTo("/error401"); 46 | return; 47 | } 48 | catch (HttpForbiddenException) 49 | { 50 | UriHelper.NavigateTo("/error403"); 51 | return; 52 | } 53 | catch (HttpInternalServerErrorException) 54 | { 55 | UriHelper.NavigateTo("/error500"); 56 | return; 57 | } 58 | 59 | UriHelper.NavigateTo("edit/todos"); 60 | } 61 | 62 | private void CancelClicked() 63 | { 64 | UriHelper.NavigateTo("/edit/todos"); 65 | } 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Shared/SlideSwitch.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 42 | 43 |
44 | 45 |
46 | 47 | 48 | @functions { 49 | 50 | [Parameter] 51 | protected Action CheckedChanged { get; set; } 52 | 53 | [Parameter] 54 | protected bool CheckedState 55 | { 56 | get 57 | { 58 | return _checkedState; 59 | } 60 | set 61 | { 62 | _checkedState = value; 63 | } 64 | } 65 | 66 | [Parameter] 67 | protected string SwitchOffColor { get; set; } 68 | 69 | [Parameter] 70 | protected string SwitchOnColor { get; set; } 71 | 72 | private bool _checkedState; 73 | 74 | bool InternalCheckedState 75 | { 76 | get 77 | { 78 | return _checkedState; 79 | } 80 | set 81 | { 82 | _checkedState = value; 83 | CheckedChanged?.Invoke(value); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/EditTodo.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/edit/todo/{todoId}" 11 | 12 | 13 | 14 | @functions { 15 | 16 | [Parameter] 17 | string TodoId { get; set; } 18 | 19 | private Todo loadedTodo; 20 | TodoForm todoForm; 21 | 22 | private readonly List allowedRoles = new List() { "todoedit" }; 23 | NavigationFailReason failReason = NavigationFailReason.Unknown; 24 | 25 | protected override async Task OnInitAsync() 26 | { 27 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 28 | { 29 | if (failReason == NavigationFailReason.RoleNotAllowed) 30 | { 31 | UriHelper.NavigateTo("/error403"); 32 | return; 33 | } 34 | return; 35 | } 36 | 37 | try 38 | { 39 | loadedTodo = await Http.GetJsonAsync($"api/todos/retrieve/{TodoId}"); 40 | } 41 | catch(HttpUnauthorizedException) 42 | { 43 | UriHelper.NavigateTo("/error401"); 44 | return; 45 | } 46 | catch(HttpForbiddenException) 47 | { 48 | UriHelper.NavigateTo("/error403"); 49 | return; 50 | } 51 | catch (HttpInternalServerErrorException) 52 | { 53 | UriHelper.NavigateTo("/error500"); 54 | return; 55 | } 56 | 57 | } 58 | 59 | private async void SaveClicked() 60 | { 61 | await Http.SendJsonAsync(HttpMethod.Post, "api/todos/update", todoForm.TodoData); 62 | UriHelper.NavigateTo("edit/todos"); 63 | } 64 | 65 | private void CancelClicked() 66 | { 67 | UriHelper.NavigateTo("/edit/todos"); 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/AddUser.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/add/user" 11 | 12 | 13 | 14 | @functions { 15 | 16 | [Parameter] 17 | string UserId { get; set; } 18 | 19 | private User newUser; 20 | UserForm userForm; 21 | private readonly List allowedRoles = new List() { "useredit", "admin" }; 22 | NavigationFailReason failReason = NavigationFailReason.Unknown; 23 | 24 | protected override async Task OnInitAsync() 25 | { 26 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 27 | { 28 | if (failReason == NavigationFailReason.RoleNotAllowed) 29 | { 30 | UriHelper.NavigateTo("/error403"); 31 | return; 32 | } 33 | return; 34 | } 35 | 36 | // If we get here we're allowed to access this page 37 | newUser = new User(); 38 | } 39 | 40 | private async void SaveClicked() 41 | { 42 | try 43 | { 44 | await Http.SendJsonAsync(HttpMethod.Post, "api/users/create", userForm.UserData); 45 | } 46 | catch (HttpUnauthorizedException) 47 | { 48 | UriHelper.NavigateTo("/error401"); 49 | return; 50 | } 51 | catch (HttpForbiddenException) 52 | { 53 | UriHelper.NavigateTo("/error403"); 54 | return; 55 | } 56 | catch (HttpInternalServerErrorException) 57 | { 58 | UriHelper.NavigateTo("/error500"); 59 | return; 60 | } 61 | 62 | UriHelper.NavigateTo("edit/users"); 63 | } 64 | 65 | private void CancelClicked() 66 | { 67 | UriHelper.NavigateTo("/edit/users"); 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Server.Services; 2 | using blazor.jwttest.Server.Services.Exceptions; 3 | using blazor.jwttest.Shared; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Collections.Generic; 6 | 7 | namespace blazor.jwttest.Server.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | public class UsersController : Controller 11 | { 12 | private Users _users; 13 | 14 | public UsersController(Users users) 15 | { 16 | _users = users; 17 | } 18 | 19 | [HttpGet("[action]")] 20 | public IEnumerable All() 21 | { 22 | var allUsers = _users.FetchAll(); 23 | return allUsers; 24 | 25 | } 26 | 27 | [HttpPost] 28 | [Route("[action]")] 29 | public IActionResult Create([FromBody]User user) 30 | { 31 | User newUser = null; 32 | if (ModelState.IsValid) 33 | newUser = _users.Add(user); 34 | 35 | if (newUser == null) 36 | return BadRequest(); 37 | 38 | return Ok(newUser); 39 | } 40 | 41 | [HttpGet("[action]/{userId:int}")] 42 | public IActionResult Retrieve(int userId) 43 | { 44 | try 45 | { 46 | var thisUser = _users.FetchSingle(userId); 47 | return Ok(thisUser); 48 | } 49 | catch(EntityNotFoundException) 50 | { 51 | return NotFound(); 52 | } 53 | } 54 | 55 | [HttpPost] 56 | [Route("[action]")] 57 | public IActionResult Update([FromBody]User user) 58 | { 59 | if (ModelState.IsValid) 60 | _users.Update(user.Id, user); 61 | 62 | return Ok(user); 63 | } 64 | 65 | [HttpPost] 66 | [Route("[action]")] 67 | public IActionResult Delete([FromBody]User user) 68 | { 69 | try 70 | { 71 | if (ModelState.IsValid) 72 | _users.Delete(user.Id); 73 | 74 | return NoContent(); 75 | } 76 | catch(EntityNotFoundException) 77 | { 78 | return NotFound(); 79 | } 80 | } 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/EditUser.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/edit/user/{userId}" 11 | 12 | 13 | 14 | @functions { 15 | 16 | [Parameter] 17 | string UserId { get; set; } 18 | 19 | private User loadedUser; 20 | UserForm userForm; 21 | private readonly List allowedRoles = new List() { "useredit", "admin" }; 22 | NavigationFailReason failReason = NavigationFailReason.Unknown; 23 | 24 | protected override async Task OnInitAsync() 25 | { 26 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 27 | { 28 | if (failReason == NavigationFailReason.RoleNotAllowed) 29 | { 30 | UriHelper.NavigateTo("/error403"); 31 | return; 32 | } 33 | return; 34 | } 35 | 36 | // If we get here we're allowed to access this page 37 | loadedUser = await Http.GetJsonAsync($"api/users/retrieve/{UserId}"); 38 | } 39 | 40 | private async void SaveClicked() 41 | { 42 | try 43 | { 44 | await Http.SendJsonAsync(HttpMethod.Post, "api/users/update", userForm.UserData); 45 | } 46 | catch (HttpUnauthorizedException) 47 | { 48 | UriHelper.NavigateTo("/error401"); 49 | return; 50 | } 51 | catch (HttpForbiddenException) 52 | { 53 | UriHelper.NavigateTo("/error403"); 54 | return; 55 | } 56 | catch (HttpInternalServerErrorException) 57 | { 58 | UriHelper.NavigateTo("/error500"); 59 | return; 60 | } 61 | 62 | UriHelper.NavigateTo("edit/users"); 63 | } 64 | 65 | private void CancelClicked() 66 | { 67 | UriHelper.NavigateTo("/edit/users"); 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Shared/NavMenu.cshtml: -------------------------------------------------------------------------------- 1 | @using blazor.jwttest.Client.Classes 2 | @inject ApplicationState AppState 3 | 4 | 10 | 11 |
12 | 32 | 33 | @if(AppState.IsLoggedIn) 34 | { 35 |
36 |

Full Name : @AppState.FullName

37 |

Email : @AppState.Email

38 |

Login Name : @AppState.UserName

39 |

Roles : 40 |

    41 | @foreach(var role in AppState.UserRoles) 42 | { 43 |
  • @role
  • 44 | } 45 |
46 |

47 |
48 | } 49 | 50 |
51 | 52 | @functions { 53 | 54 | bool collapseNavMenu = true; 55 | 56 | protected override void OnInit() 57 | { 58 | AppState.LoginSucceeded += UpdateState; 59 | AppState.LogoutSucceeded += UpdateState; 60 | base.OnInit(); 61 | } 62 | 63 | void ToggleNavMenu() 64 | { 65 | collapseNavMenu = !collapseNavMenu; 66 | } 67 | 68 | private void UpdateState(object sender, string unused) 69 | { 70 | StateHasChanged(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/Login.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Client.Classes 3 | @using blazor.jwttest.Shared 4 | @using System.Threading; 5 | 6 | @inject IUriHelper UriHelper 7 | @inject ApplicationState _appState 8 | 9 | @page "/login" 10 | 11 |
12 | 13 |
14 |
15 |

Please enter your user name and password

16 |
17 |
18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 | 34 | 41 | 42 | 43 | @functions { 44 | 45 | protected LoginDetails LoginDetails { get; set; } = new LoginDetails(); 46 | protected bool ShowLoginFailed { get; set; } 47 | 48 | private bool showPopup { get; set; } = false; 49 | private Timer _timer; 50 | 51 | 52 | protected async Task LoginToApplication() 53 | { 54 | await _appState.Login(LoginDetails); 55 | 56 | if (_appState.IsLoggedIn) 57 | { 58 | UriHelper.NavigateTo("/"); 59 | } 60 | else 61 | { 62 | ShowLoginFailed = true; 63 | _timer = new Timer(TimerCallBack, null, 5000, Timeout.Infinite); 64 | } 65 | } 66 | 67 | void TimerCallBack(object state) 68 | { 69 | ShowLoginFailed = false; 70 | StateHasChanged(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Controllers/TodoController.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Server.Services; 2 | using blazor.jwttest.Server.Services.Exceptions; 3 | using blazor.jwttest.Shared; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using System.Collections.Generic; 7 | 8 | namespace blazor.jwttest.Server.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | public class TodosController : Controller 12 | { 13 | private Todos _todos; 14 | 15 | public TodosController(Todos todos) 16 | { 17 | _todos = todos; 18 | } 19 | 20 | [HttpGet("[action]")] 21 | public IEnumerable All() 22 | { 23 | var allTodos = _todos.FetchAll(); 24 | return allTodos; 25 | 26 | } 27 | 28 | [HttpPost] 29 | [Route("[action]")] 30 | [Authorize(Roles = "todoedit")] 31 | public IActionResult Create([FromBody]Todo todo) 32 | { 33 | Todo newTodo = null; 34 | if (ModelState.IsValid) 35 | newTodo = _todos.Add(todo); 36 | 37 | if (newTodo == null) 38 | return BadRequest(); 39 | 40 | return Ok(newTodo); 41 | } 42 | 43 | [HttpGet("[action]/{todoId:int}")] 44 | [Authorize(Roles = "todoedit")] 45 | public IActionResult Retrieve(int todoId) 46 | { 47 | try 48 | { 49 | var thisTodo = _todos.FetchSingle(todoId); 50 | return Ok(thisTodo); 51 | } 52 | catch(EntityNotFoundException) 53 | { 54 | return NotFound(); 55 | } 56 | } 57 | 58 | [HttpPost] 59 | [Route("[action]")] 60 | [Authorize(Roles = "todoedit")] 61 | public IActionResult Update([FromBody]Todo todo) 62 | { 63 | if (ModelState.IsValid) 64 | _todos.Update(todo.Id, todo); 65 | 66 | return Ok(todo); 67 | } 68 | 69 | [HttpPost] 70 | [Route("[action]")] 71 | [Authorize(Roles = "todoedit")] 72 | public IActionResult Delete([FromBody]Todo todo) 73 | { 74 | try 75 | { 76 | if (ModelState.IsValid) 77 | _todos.Delete(todo.Id); 78 | 79 | return NoContent(); 80 | } 81 | catch(EntityNotFoundException) 82 | { 83 | return NotFound(); 84 | } 85 | } 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Shared/TodoForm.cshtml: -------------------------------------------------------------------------------- 1 | @using blazor.jwttest.Shared 2 | @using blazor.jwttest.Client.Classes 3 | @inject ApplicationState AppState 4 | 5 |
6 | 7 |

8 | 9 | @if (TodoInfo == null) 10 | { 11 |

@LoadingMessage

12 | } 13 | else 14 | { 15 |
Editing Todo ID = @TodoInfo.Id
16 |
17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | } 42 | 43 |
44 | 45 | 46 | @functions { 47 | 48 | //TODO: Add some kind of validation service 49 | 50 | [Parameter] 51 | protected Todo TodoInfo { get; set; } 52 | 53 | [Parameter] 54 | private string LoadingMessage { get; set; } 55 | 56 | [Parameter] 57 | private Action OnSaveClicked { get; set; } 58 | 59 | [Parameter] 60 | private Action OnCancelClicked { get; set; } 61 | 62 | public Todo TodoData { get; private set; } 63 | 64 | private void SaveButtonClicked() 65 | { 66 | TodoData = TodoInfo; 67 | OnSaveClicked?.Invoke(); 68 | } 69 | 70 | private void CancelButtonClicked() 71 | { 72 | OnCancelClicked?.Invoke(); 73 | } 74 | 75 | private void DoneStatusChanged(bool value) 76 | { 77 | TodoInfo.Done = value; 78 | StateHasChanged(); 79 | 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/EditUsers.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/edit/users" 11 | 12 |

Edit Users's

13 | 14 | @if (users == null) 15 | { 16 |

Loading list...

17 | } 18 | else 19 | { 20 | Add New User
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | @foreach (var user in users) 31 | { 32 | 33 | 34 | 35 | 39 | 40 | } 41 | 42 |
#Name 
@user.Id@user.LoginName 36 | Edit User 37 | Delete User 38 |
43 | } 44 | 45 | @functions { 46 | 47 | User[] users; 48 | 49 | private readonly List allowedRoles = new List() { "useredit", "admin" }; 50 | NavigationFailReason failReason = NavigationFailReason.Unknown; 51 | 52 | protected override async Task OnInitAsync() 53 | { 54 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 55 | { 56 | if (failReason == NavigationFailReason.RoleNotAllowed) 57 | { 58 | UriHelper.NavigateTo("/error403"); 59 | return; 60 | } 61 | return; 62 | } 63 | 64 | // If we get here we're allowed to access this page 65 | try 66 | { 67 | users = await Http.GetJsonAsync("api/users/all"); 68 | } 69 | catch (HttpUnauthorizedException) 70 | { 71 | UriHelper.NavigateTo("/error401"); 72 | return; 73 | } 74 | catch (HttpForbiddenException) 75 | { 76 | UriHelper.NavigateTo("/error403"); 77 | return; 78 | } 79 | catch (HttpInternalServerErrorException) 80 | { 81 | UriHelper.NavigateTo("/error500"); 82 | return; 83 | } 84 | 85 | } 86 | 87 | } 88 | 89 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/DeleteTodo.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/delete/todo/{todoId}" 11 | 12 |
13 | 14 |

Are you Sure you want to delete this todo?

15 |

This operation is permenent and cannot be undone.

16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 | @functions { 30 | 31 | [Parameter] 32 | string TodoId { get; set; } 33 | 34 | private readonly List allowedRoles = new List() { "todoedit" }; 35 | NavigationFailReason failReason = NavigationFailReason.Unknown; 36 | 37 | protected override void OnInit() 38 | { 39 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 40 | { 41 | if (failReason == NavigationFailReason.RoleNotAllowed) 42 | { 43 | UriHelper.NavigateTo("/error403"); 44 | return; 45 | } 46 | return; 47 | } 48 | } 49 | 50 | private async void YesClicked() 51 | { 52 | User user = new User(); 53 | user.Id = Convert.ToInt32(TodoId); 54 | 55 | try 56 | { 57 | await Http.SendJsonAsync(HttpMethod.Post, "api/todos/delete", user); 58 | } 59 | catch (HttpUnauthorizedException) 60 | { 61 | UriHelper.NavigateTo("/error401"); 62 | return; 63 | } 64 | catch (HttpForbiddenException) 65 | { 66 | UriHelper.NavigateTo("/error403"); 67 | return; 68 | } 69 | catch (HttpInternalServerErrorException) 70 | { 71 | UriHelper.NavigateTo("/error500"); 72 | return; 73 | } 74 | 75 | UriHelper.NavigateTo("edit/todos"); 76 | 77 | } 78 | 79 | private void NoClicked() 80 | { 81 | UriHelper.NavigateTo("/edit/todos"); 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/DeleteUser.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/delete/user/{userId}" 11 | 12 |
13 | 14 |

Are you Sure you want to delete this user?

15 |

This operation is permenent and cannot be undone.

16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 | @functions { 30 | 31 | [Parameter] 32 | string UserId { get; set; } 33 | 34 | private readonly List allowedRoles = new List() { "useredit", "admin" }; 35 | NavigationFailReason failReason = NavigationFailReason.Unknown; 36 | 37 | protected override async Task OnInitAsync() 38 | { 39 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 40 | { 41 | if (failReason == NavigationFailReason.RoleNotAllowed) 42 | { 43 | UriHelper.NavigateTo("/error403"); 44 | return; 45 | } 46 | return; 47 | } 48 | } 49 | 50 | private async void YesClicked() 51 | { 52 | User user = new User(); 53 | user.Id = Convert.ToInt32(UserId); 54 | 55 | try 56 | { 57 | await Http.SendJsonAsync(HttpMethod.Post, "api/users/delete", user); 58 | } 59 | catch (HttpUnauthorizedException) 60 | { 61 | UriHelper.NavigateTo("/error401"); 62 | return; 63 | } 64 | catch (HttpForbiddenException) 65 | { 66 | UriHelper.NavigateTo("/error403"); 67 | return; 68 | } 69 | catch (HttpInternalServerErrorException) 70 | { 71 | UriHelper.NavigateTo("/error500"); 72 | return; 73 | } 74 | 75 | UriHelper.NavigateTo("edit/users"); 76 | 77 | } 78 | 79 | private void NoClicked() 80 | { 81 | UriHelper.NavigateTo("/edit/users"); 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blazor.jwttest 2 | Quick test using JWT authentication for a blazor hosted (Client/Serverside) app with API and Authentication. 3 | 4 | Nothing Special, it simply has a login form, a changing nav bar based on login state, a small PostgreSQL based data layer that's designed to work with a postgres data base via EF Core (But should work with any DB that EF core supports) 5 | 6 | It's not designed to be a mainstream project, it's me learning Blazor and trying to adopt the same ideas and programming model I already use for my Aurelia/.NET Core applications. 7 | 8 | The template is more or less feature complete to what I wanted to make it do, it does however still run on Blazor 0.6.0 (See issue 1 in the issues for the reason) 9 | 10 | ## features 11 | * JWT based token authentication 12 | * Roles from user record in database are used to prevent navigation to pages the logged in user does not have access too, and also protect the rest endpoints in the server 13 | * Custom component's and event based communication to parent pages 14 | * Generic design where permitted 15 | * all cross platform, dotnet core throughout 16 | * Entity framework datalayer, which will build initial database if run on an empty DB server, and seed data 17 | * Singleton based application state, accessible throughout the app 18 | 19 | ## Note 20 | This MUST be run on an empty PostgreSQL database. You'll need to update the connection string in the app settings file in the server project, then when run the app will create the two tables it needs and seed an initial user called "admin" with password "letmein" 21 | 22 | If there are ANY objects at all in the DB your running against, the create will fail, and then when you run the app, you'll get errors about the tables not existing, I'll add some SQL scripts later on for those who want to create tables manually. 23 | 24 | This should be useable against other DB's (since it uses EF) but I've not tested it, so you'll need to do some work yourself for that. It's all portable code however, and this is really just me playing around with and testing Blazor out, so don't expect amazing code :-) 25 | 26 | ## Credit 27 | Credit where credit is due, some large chunks of the code in here came from Chris Saintly at Codedaze.io and his article on doing JWT in a blazor app, I'd also like to say thanks to @SQL-MisterMagoo and @kswoll in the Blazor gitter group for pointers on component communication, and overriding the HTTP client to get better error handling. 28 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/Users.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Server.Database; 2 | using blazor.jwttest.Server.Database.Entities; 3 | using blazor.jwttest.Server.Services.Exceptions; 4 | using blazor.jwttest.Shared; 5 | using Mapster; 6 | using System; 7 | using System.Linq; 8 | 9 | namespace blazor.jwttest.Server.Services 10 | { 11 | public class Users : GenericDbAccess 12 | { 13 | public Users(EfDataContext db) : base(db) 14 | {} 15 | 16 | public override void ApplyCustomMappings() 17 | { 18 | // From VM to DB 19 | TypeAdapterConfig.NewConfig() 20 | .IgnoreNullValues(true) // If any data passed in is null in the source DO NOT change the destination 21 | .AfterMapping((src, dest) => { 22 | if(!String.IsNullOrEmpty(src.Password)) 23 | { 24 | // If the password field in the source object is not empty, then encrypt it in the destination 25 | // before the record is saved into the database. 26 | // NOTE: If you DO NOT want to change the users password, or want to keep it intact, then you MUST 27 | // make sure that the source password property is null or empty before it hits this service. 28 | dest.Password = BCrypt.Net.BCrypt.HashPassword(src.Password); 29 | } 30 | }); 31 | 32 | // From DB to VM 33 | TypeAdapterConfig.NewConfig() 34 | .Ignore(dest => dest.Password); // Don't allow password to be retrieved from DB 35 | 36 | } 37 | 38 | // Custom functions for the User Service Go Here 39 | public User ValidateLogin(string loginName, string password) 40 | { 41 | // This call uses the DB context directly, as we need to validate the encrypted password 42 | // using BCrypt to verify that the login details are correct. For retrieving/updating etc 43 | // you should always use the methods in the base class (See GenericDbAccess.cs) 44 | DbUser foundUser = _db.Users.FirstOrDefault(r => r.LoginName == loginName); 45 | if (foundUser == null) throw new UserNotValidatedException(ValidationFailReason.UserNotFound, loginName); 46 | 47 | if(!BCrypt.Net.BCrypt.Verify(password, foundUser.Password)) 48 | { 49 | throw new UserNotValidatedException(ValidationFailReason.PasswordDoesNotMatch, loginName); 50 | } 51 | 52 | User userToReturn = foundUser.Adapt(); 53 | 54 | return userToReturn; 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Shared/UserForm.cshtml: -------------------------------------------------------------------------------- 1 | @using blazor.jwttest.Shared 2 | @using blazor.jwttest.Client.Classes 3 | @inject ApplicationState AppState 4 | 5 |
6 | 7 |

8 | 9 | @if (UserInfo == null) 10 | { 11 |

@LoadingMessage

12 | } 13 | else 14 | { 15 |
Editing User [@UserInfo.LoginName]
16 |
17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | } 52 | 53 |
54 | 55 | 56 | @functions { 57 | 58 | //TODO: Add some kind of validation service 59 | 60 | [Parameter] 61 | private User UserInfo { get; set; } 62 | 63 | [Parameter] 64 | private string LoadingMessage { get; set; } 65 | 66 | [Parameter] 67 | private bool MakeIdReadOnly { get; set; } 68 | 69 | [Parameter] 70 | private Action OnSaveClicked { get; set; } 71 | 72 | [Parameter] 73 | private Action OnCancelClicked { get; set; } 74 | 75 | private RolesEditor roleeditor; 76 | 77 | public User UserData { get; private set; } 78 | 79 | private void SaveButtonClicked() 80 | { 81 | UserData = UserInfo; 82 | UserData.AllowedRoles = roleeditor.RolesList.ToArray(); 83 | OnSaveClicked?.Invoke(); 84 | } 85 | 86 | private void CancelButtonClicked() 87 | { 88 | OnCancelClicked?.Invoke(); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | app { 8 | position: relative; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | 13 | .top-row { 14 | height: 3.5rem; 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .main { 20 | flex: 1; 21 | } 22 | 23 | .main .top-row { 24 | background-color: #e6e6e6; 25 | border-bottom: 1px solid #d6d5d5; 26 | } 27 | 28 | .sidebar { 29 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 30 | } 31 | 32 | .sidebar .top-row { 33 | background-color: rgba(0,0,0,0.4); 34 | } 35 | 36 | .sidebar .navbar-brand { 37 | font-size: 1.1rem; 38 | } 39 | 40 | .sidebar .oi { 41 | width: 2rem; 42 | font-size: 1.1rem; 43 | vertical-align: text-top; 44 | top: -2px; 45 | } 46 | 47 | .nav-item { 48 | font-size: 0.9rem; 49 | padding-bottom: 0.5rem; 50 | } 51 | 52 | .nav-item:first-of-type { 53 | padding-top: 1rem; 54 | } 55 | 56 | .nav-item:last-of-type { 57 | padding-bottom: 1rem; 58 | } 59 | 60 | .nav-item a { 61 | color: #d7d7d7; 62 | border-radius: 4px; 63 | height: 3rem; 64 | display: flex; 65 | align-items: center; 66 | line-height: 3rem; 67 | } 68 | 69 | .nav-item a.active { 70 | background-color: rgba(255,255,255,0.25); 71 | color: white; 72 | } 73 | 74 | .nav-item a:hover { 75 | background-color: rgba(255,255,255,0.1); 76 | color: white; 77 | } 78 | 79 | .content { 80 | padding-top: 1.1rem; 81 | } 82 | 83 | .navbar-toggler { 84 | background-color: rgba(255, 255, 255, 0.1); 85 | } 86 | 87 | @media (max-width: 767.98px) { 88 | .main .top-row { 89 | display: none; 90 | } 91 | } 92 | 93 | @media (min-width: 768px) { 94 | app { 95 | flex-direction: row; 96 | } 97 | 98 | .sidebar { 99 | width: 250px; 100 | height: 100vh; 101 | position: sticky; 102 | top: 0; 103 | } 104 | 105 | .main .top-row { 106 | position: sticky; 107 | top: 0; 108 | } 109 | 110 | .main > div { 111 | padding-left: 2rem !important; 112 | padding-right: 1.5rem !important; 113 | } 114 | 115 | .navbar-toggler { 116 | display: none; 117 | } 118 | 119 | .sidebar .collapse { 120 | /* Never collapse the sidebar for wide screens */ 121 | display: block; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/blazor.jwttest.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Exe 6 | 7.3 7 | dotnet 8 | blazor serve 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | $(IncludeRazorContentInPack) 29 | 30 | 31 | $(IncludeRazorContentInPack) 32 | 33 | 34 | $(IncludeRazorContentInPack) 35 | 36 | 37 | $(IncludeRazorContentInPack) 38 | 39 | 40 | $(IncludeRazorContentInPack) 41 | 42 | 43 | $(IncludeRazorContentInPack) 44 | 45 | 46 | $(IncludeRazorContentInPack) 47 | 48 | 49 | $(IncludeRazorContentInPack) 50 | 51 | 52 | $(IncludeRazorContentInPack) 53 | 54 | 55 | $(IncludeRazorContentInPack) 56 | 57 | 58 | $(IncludeRazorContentInPack) 59 | 60 | 61 | $(IncludeRazorContentInPack) 62 | 63 | 64 | $(IncludeRazorContentInPack) 65 | 66 | 67 | $(IncludeRazorContentInPack) 68 | 69 | 70 | $(IncludeRazorContentInPack) 71 | 72 | 73 | $(IncludeRazorContentInPack) 74 | 75 | 76 | $(IncludeRazorContentInPack) 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Pages/EditTodos.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Blazor.Services 2 | @using blazor.jwttest.Shared 3 | @using blazor.jwttest.Client.Classes 4 | @using blazor.jwttest.Client.Exceptions 5 | 6 | @inject HttpClient Http 7 | @inject IUriHelper UriHelper 8 | @inject ApplicationState AppState 9 | 10 | @page "/edit/todos" 11 | 12 |

Edit Todo's

13 | 14 | @if (todos == null) 15 | { 16 |

Loading list...

17 | } 18 | else 19 | { 20 | Add New Todo
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | @foreach (var todo in todos) 32 | { 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | } 44 | 45 |
#TitleDone 
@todo.Id@todo.Title@todo.Done 38 | Edit Todo 39 | Delete Todo 40 |
46 | } 47 | 48 | @functions { 49 | 50 | Todo[] todos; 51 | 52 | private readonly List allowedRoles = new List() { "todoedit", "useredit" }; 53 | NavigationFailReason failReason = NavigationFailReason.Unknown; 54 | 55 | protected override async Task OnInitAsync() 56 | { 57 | // Routine to handle page navigation is in the ApplicationState class. In Time I'll work out a better way of doing this 58 | // but for now, it takes 2 required parameters and one optional. 59 | // 60 | // Required 1 : 'allowedRoles' - List of roles required to access this page 61 | // Required 2 : 'failReason' - member of enumeration NavigationFailReason used to communicate fail reason back to caller 62 | // 63 | // Optional 1 : 'notLoggedInRoute' - default empty string, if default left, will NOT redirect on not being logged in 64 | // if default changed, will try to redirect to that route if not logged in 65 | // 66 | // Returns boolean - true = allowed to stay on this page, false = not allowed 67 | 68 | if (!AppState.IsAllowedToNavigate(allowedRoles, out failReason, "/login")) 69 | { 70 | // In this example, if we are not logged in, we'll go straight to login route, otherwise we'll return false 71 | // end up in this "if" with a fail reason of 'RoleNotAllowed' 72 | if (failReason == NavigationFailReason.RoleNotAllowed) 73 | { 74 | UriHelper.NavigateTo("/error403"); 75 | return; 76 | } 77 | return; 78 | } 79 | 80 | // If we get here we're allowed to access this page 81 | try 82 | { 83 | todos = await Http.GetJsonAsync("api/todos/all"); 84 | } 85 | catch (HttpUnauthorizedException) 86 | { 87 | UriHelper.NavigateTo("/error401"); 88 | return; 89 | } 90 | catch (HttpForbiddenException) 91 | { 92 | UriHelper.NavigateTo("/error403"); 93 | return; 94 | } 95 | catch (HttpInternalServerErrorException) 96 | { 97 | UriHelper.NavigateTo("/error500"); 98 | return; 99 | } 100 | 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Controllers/AuthenticationController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IdentityModel.Tokens.Jwt; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Text; 7 | using blazor.jwttest.Server.Services; 8 | using blazor.jwttest.Server.Services.Exceptions; 9 | using blazor.jwttest.Shared; 10 | using Microsoft.AspNetCore.Authorization; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Configuration; 13 | using Microsoft.IdentityModel.Tokens; 14 | 15 | namespace gisportal.Server.Controllers 16 | { 17 | [Route("api/[controller]")] 18 | public class AuthenticationController : Controller 19 | { 20 | private readonly IConfiguration _configuration; 21 | private readonly Users _users; 22 | 23 | public AuthenticationController( 24 | IConfiguration configuration, 25 | Users users 26 | ) 27 | { 28 | _configuration = configuration; 29 | _users = users; 30 | } 31 | 32 | [HttpPost("[action]")] 33 | [AllowAnonymous] 34 | public IActionResult Login([FromBody] LoginDetails login) 35 | { 36 | User thisUser = null; 37 | try 38 | { 39 | thisUser = _users.ValidateLogin(login.Username, login.Password); 40 | } 41 | catch (UserNotValidatedException) 42 | { 43 | // NOTE: If you want to check the exception at this point, there's a validation reason on it that tells you 44 | // what actually failed, however it's generally good practice not to tell that info to the UI as it gives 45 | // anyone trying to gain access maliciously and idea of what's right and what's not :-) 46 | return BadRequest("Username and password are invalid."); 47 | } 48 | 49 | DateTime issueTime = DateTime.UtcNow; 50 | 51 | // Add required and basic JWT claims to the token 52 | List claims = new List 53 | { 54 | new Claim(JwtRegisteredClaimNames.Sub, thisUser.LoginName), 55 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 56 | new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(DateTime.UtcNow).ToUniversalTime().ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), 57 | new Claim(JwtRegisteredClaimNames.Email, thisUser.Email), 58 | new Claim(JwtRegisteredClaimNames.GivenName, thisUser.FullName) 59 | }; 60 | 61 | // Add our users roles to the JWT 62 | // DO NOT Change this claim name, ASP.NET core role auth requires this exact claim name for controller roles 63 | claims.AddRange( 64 | thisUser.AllowedRoles 65 | .Select(role => new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role", role, ClaimValueTypes.String))); 66 | 67 | // Build the actual token 68 | int expiryLengthInMinutes = Convert.ToInt32(_configuration["JwtExpiryInMinutes"]); 69 | DateTime now = DateTime.UtcNow; 70 | TimeSpan expirationTime = new TimeSpan(0, expiryLengthInMinutes, 0); 71 | var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_configuration["JwtSecurityKey"])); 72 | var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); 73 | 74 | var jwt = new JwtSecurityToken( 75 | _configuration["JwtIssuer"], 76 | _configuration["JwtIssuer"], 77 | claims, 78 | expires: now.Add(expirationTime), 79 | signingCredentials: signingCredentials); 80 | 81 | var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); 82 | 83 | // Create response and send token back to caller 84 | var response = new 85 | { 86 | token = encodedJwt, 87 | expires_in = (int)expirationTime.TotalSeconds 88 | }; 89 | 90 | return Ok(response); 91 | 92 | } 93 | 94 | } 95 | } -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /blazor.jwttest.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.27130.2027 4 | MinimumVisualStudioVersion = 15.0.26124.0 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "blazor.jwttest.Server", "blazor.jwttest.Server\blazor.jwttest.Server.csproj", "{1498D4A9-7FF5-44B3-8799-FAF1A9285348}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "blazor.jwttest.Client", "blazor.jwttest.Client\blazor.jwttest.Client.csproj", "{0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "blazor.jwttest.Shared", "blazor.jwttest.Shared\blazor.jwttest.Shared.csproj", "{A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Debug|x64 = Debug|x64 15 | Debug|x86 = Debug|x86 16 | Release|Any CPU = Release|Any CPU 17 | Release|x64 = Release|x64 18 | Release|x86 = Release|x86 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Debug|x64.ActiveCfg = Debug|Any CPU 24 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Debug|x64.Build.0 = Debug|Any CPU 25 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Debug|x86.ActiveCfg = Debug|Any CPU 26 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Debug|x86.Build.0 = Debug|Any CPU 27 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Release|x64.ActiveCfg = Release|Any CPU 30 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Release|x64.Build.0 = Release|Any CPU 31 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Release|x86.ActiveCfg = Release|Any CPU 32 | {1498D4A9-7FF5-44B3-8799-FAF1A9285348}.Release|x86.Build.0 = Release|Any CPU 33 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Debug|x64.Build.0 = Debug|Any CPU 37 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Debug|x86.ActiveCfg = Debug|Any CPU 38 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Debug|x86.Build.0 = Debug|Any CPU 39 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Release|x64.ActiveCfg = Release|Any CPU 42 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Release|x64.Build.0 = Release|Any CPU 43 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Release|x86.ActiveCfg = Release|Any CPU 44 | {0C73EFC2-A0E3-47F9-A37F-B8D4AB2295F7}.Release|x86.Build.0 = Release|Any CPU 45 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Debug|x64.ActiveCfg = Debug|Any CPU 48 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Debug|x64.Build.0 = Debug|Any CPU 49 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Debug|x86.Build.0 = Debug|Any CPU 51 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Release|x64.ActiveCfg = Release|Any CPU 54 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Release|x64.Build.0 = Release|Any CPU 55 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Release|x86.ActiveCfg = Release|Any CPU 56 | {A4B7CC61-E858-40ED-B6A0-92B4A83FE39D}.Release|x86.Build.0 = Release|Any CPU 57 | EndGlobalSection 58 | GlobalSection(SolutionProperties) = preSolution 59 | HideSolutionNode = FALSE 60 | EndGlobalSection 61 | GlobalSection(ExtensibilityGlobals) = postSolution 62 | SolutionGuid = {A173280C-4095-43BF-9876-329825CE43B2} 63 | EndGlobalSection 64 | EndGlobal 65 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Services/GenericDbAccess.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Server.Database; 2 | using blazor.jwttest.Server.Database.Entities; 3 | using blazor.jwttest.Server.Services.Exceptions; 4 | using Mapster; 5 | using Microsoft.Extensions.Logging; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Linq.Expressions; 10 | 11 | namespace blazor.jwttest.Server.Services 12 | { 13 | public class GenericDbAccess where dbT : DbEntityBase 14 | { 15 | internal readonly EfDataContext _db; 16 | //internal ILogger _logger; 17 | 18 | public GenericDbAccess(EfDataContext db) 19 | { 20 | _db = db; 21 | 22 | // Set up any global mapper mappings we have 23 | ApplyGlobalMappings(); 24 | 25 | // Call into the virtual implementation (If there is one) in the derived class 26 | // to set custom mapster mappings for that class 27 | ApplyCustomMappings(); 28 | } 29 | 30 | internal void ApplyGlobalMappings() 31 | { 32 | // Mapster Global Configuration 33 | // NOTE: These are commented out, as they are NOT used when dealing with a postgresql database, as postgres can 34 | // have columns that contain arrays natively (Great for things like roles and tags and such like). Other DB's 35 | // such as SQLite and SQL Server however cannot handle arrays, so these mapster rules when in use will (or should) 36 | // transparently convert from array to delimeted string without you realizing. All you need to do is to make sure 37 | // your view type has an array and your DbType has a string for the same named property. 38 | 39 | // The following two rules will serilize a string array to a token delimited string and back again 40 | //TypeAdapterConfig.NewConfig().MapWith(str => str.Split(',', StringSplitOptions.None)); 41 | //TypeAdapterConfig.NewConfig().MapWith(str => String.Join(',', str)); 42 | 43 | // The following two rules will serilize an IEnumerable to a token delimited string and back again 44 | //TypeAdapterConfig.NewConfig().MapWith(str => str.ConvertToIntArray()); // Use string extension method 45 | //TypeAdapterConfig.NewConfig().MapWith(ienum => String.Join(',', ienum)); 46 | } 47 | 48 | public virtual void ApplyCustomMappings() 49 | { 50 | // Empty definition in the base, but if provided in derived classes 51 | // will be called at class constructions so that mapster rules can be set up that are specific to the service 52 | // class being used. A good example is in the Users Service, where passwords can be passed in for storage 53 | // but can not be retrieved. 54 | } 55 | 56 | internal List FetchAll() 57 | { 58 | return _db.Set().OrderBy(r => r.Id).ToList().Adapt>(); 59 | } 60 | 61 | internal viewT FetchSingle(int id) 62 | { 63 | dbT existingRecord = _db.Set().Find(id); 64 | if (existingRecord == null) 65 | { 66 | throw new EntityNotFoundException(id, typeof(dbT)); 67 | } 68 | 69 | return existingRecord.Adapt(); ; 70 | } 71 | 72 | internal viewT FetchSinlgeUsingPredicate(Expression> predicate) 73 | { 74 | dbT existingRecord = _db 75 | .Set().FirstOrDefault(predicate); 76 | if (existingRecord == null) 77 | { 78 | throw new NoEntityFoundForPredicateException(); 79 | } 80 | 81 | return existingRecord.Adapt(); 82 | } 83 | 84 | internal void Update(int id, viewT updatedEntity) 85 | { 86 | dbT existingRecord = _db.Set().Find(id); 87 | if (existingRecord == null) 88 | { 89 | throw new EntityNotFoundException(id, typeof(dbT)); 90 | } 91 | 92 | existingRecord = updatedEntity.Adapt(existingRecord); 93 | existingRecord.DateModified = DateTime.UtcNow; 94 | _db.SaveChanges(); 95 | } 96 | 97 | internal viewT Add(viewT newEntity) 98 | { 99 | dbT entityToAdd = newEntity.Adapt(); 100 | 101 | entityToAdd.DateCreated = DateTime.UtcNow; 102 | 103 | _db.Set().Add(entityToAdd); 104 | _db.SaveChanges(); 105 | 106 | return entityToAdd.Adapt(); 107 | } 108 | 109 | internal void Delete(int id) 110 | { 111 | dbT existingRecord = _db.Set().Find(id); 112 | if (existingRecord == null) 113 | { 114 | throw new EntityNotFoundException(id, typeof(dbT)); 115 | } 116 | 117 | _db.Set().Remove(existingRecord); 118 | _db.SaveChanges(); 119 | } 120 | 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /blazor.jwttest.Server/Startup.cs: -------------------------------------------------------------------------------- 1 | using blazor.jwttest.Server.Database; 2 | using blazor.jwttest.Server.Services; 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Blazor.Server; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.ResponseCompression; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.IdentityModel.Tokens; 12 | using System; 13 | using System.Linq; 14 | using System.Net.Mime; 15 | using System.Text; 16 | 17 | namespace blazor.jwttest.Server 18 | { 19 | public class Startup 20 | { 21 | public IConfigurationRoot Configuration { get; } 22 | 23 | public Startup(IHostingEnvironment env) 24 | { 25 | var builder = new ConfigurationBuilder() 26 | .SetBasePath(env.ContentRootPath) 27 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 28 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) 29 | .AddEnvironmentVariables(); 30 | 31 | Configuration = builder.Build(); 32 | } 33 | 34 | public void ConfigureServices(IServiceCollection services) 35 | { 36 | // Setup DC 37 | services.AddDbContext(options => 38 | { 39 | options.UseNpgsql(Configuration.GetConnectionString("PostgresDbConnection")); 40 | }); 41 | 42 | // Setup Dependency injected services 43 | services.AddTransient(); 44 | services.AddTransient(); 45 | 46 | // Setup our JWT Token auth 47 | ConfigureTokenAuth(services); 48 | 49 | // All the rest 50 | services.AddMvc(); 51 | services.AddOptions(); 52 | services.AddResponseCompression(options => 53 | { 54 | options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] 55 | { 56 | MediaTypeNames.Application.Octet, 57 | WasmMediaTypeNames.Application.Wasm, 58 | }); 59 | }); 60 | } 61 | 62 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 63 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 64 | { 65 | using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) 66 | { 67 | // Make sure our database tables are created 68 | // NOTE: This will ONLY be acted upon if the database you are creating is EMPTY, if even just ONE table 69 | // exists, doesn't matter if it's a same name as tables in this application, then creation will be skipped 70 | // If table creation is skipped, and the table names that the entities in the datacontext look for 71 | // are missing, then you'll get an exception when you try to read and write data, telling you there are no tables. 72 | // you'll probably want to kill this and use migrations or something for a larger app, THIS IS JUST FOR TESTING 73 | serviceScope.ServiceProvider.GetService().Database.EnsureCreated(); 74 | 75 | // This small snippet of SQL is specific to PostgreSQL databases only. Beacuse of the way postgre sequences work 76 | // we need to advance the sequence by one to account for the data we seed into the users table, so that the next 77 | // record inserted has it's integer ID start at the correct place. 78 | // if you add more records in the SeedData function in the Ef data context, this statement will have to be altered 79 | // accordingly. 80 | serviceScope.ServiceProvider.GetService().Database.ExecuteSqlCommand("select nextval('users_id_seq')"); 81 | 82 | } 83 | 84 | app.UseAuthentication(); 85 | 86 | app.UseResponseCompression(); 87 | 88 | if (env.IsDevelopment()) 89 | { 90 | app.UseDeveloperExceptionPage(); 91 | } 92 | 93 | app.UseMvc(routes => 94 | { 95 | routes.MapRoute(name: "default", template: "{controller}/{action}/{id?}"); 96 | }); 97 | 98 | app.UseBlazor(); 99 | } 100 | 101 | // Private function to configure our JWT Authentication on the server 102 | private void ConfigureTokenAuth(IServiceCollection services) 103 | { 104 | services.AddAuthentication(options => 105 | { 106 | options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; 107 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 108 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 109 | }) 110 | .AddJwtBearer(config => 111 | { 112 | config.RequireHttpsMetadata = false; 113 | config.SaveToken = true; 114 | config.TokenValidationParameters = new TokenValidationParameters() 115 | { 116 | ValidateIssuerSigningKey = true, 117 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["JwtSecurityKey"])), 118 | ValidateIssuer = true, 119 | ValidIssuer = Configuration["JwtIssuer"], 120 | ValidateAudience = true, 121 | ValidAudience = Configuration["JwtIssuer"], 122 | ValidateLifetime = true, 123 | ClockSkew = TimeSpan.Zero 124 | }; 125 | 126 | }); 127 | } 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Classes/JwtDecode.cs: -------------------------------------------------------------------------------- 1 | using Blazor.Extensions.Storage; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace blazor.jwttest.Client.Classes 10 | { 11 | public class JwtDecode 12 | { 13 | // TODO: Figure out how to get the JWT Auth .net libs into blazor withoout totaly screwing things up 14 | // and replace all the JwtClaim names in this file with proper constants. 15 | 16 | private string _header { get; set; } 17 | private string _payload { get; set; } 18 | private string _token { get; set; } 19 | private LocalStorage _localStorage { get; } 20 | private string _authTokenName { get; set; } 21 | 22 | public JwtDecode(LocalStorage localStorage) 23 | { 24 | _localStorage = localStorage; 25 | } 26 | 27 | private void DecodeToken() 28 | { 29 | string[] parts = _token.Split('.'); 30 | _header = parts[0]; 31 | _payload = parts[1]; 32 | 33 | if (_header.Length % 4 != 0) // B64 strings must be a length that is a multiple of 4 to decode them 34 | { 35 | var lengthToBe = _header.Length.GetNextHighestMultiple(4); 36 | _header = _header.PadRight(lengthToBe, '='); 37 | } 38 | 39 | if (_payload.Length % 4 != 0) // B64 strings must be a length that is a multiple of 4 to decode them 40 | { 41 | var lengthToBe = _payload.Length.GetNextHighestMultiple(4); 42 | _payload = _payload.PadRight(lengthToBe, '='); 43 | } 44 | 45 | } 46 | 47 | public async Task LoadToken(string tokenName) 48 | { 49 | _authTokenName = tokenName; 50 | 51 | _token = await _localStorage.GetItem(_authTokenName); 52 | return true; 53 | } 54 | 55 | public Dictionary GetPayload() 56 | { 57 | DecodeToken(); 58 | 59 | var payload = Encoding.UTF8.GetString(Convert.FromBase64String(_payload)); 60 | var data = JsonConvert.DeserializeObject>(payload); 61 | 62 | // Testing 63 | //foreach(var key in data) 64 | //{ 65 | // Console.WriteLine($"PAYLOAD: {key.Key} = {key.Value} (TYP: {key.Value.GetType().ToString()})"); 66 | //} 67 | 68 | return data; 69 | } 70 | 71 | public Dictionary GetHeader() 72 | { 73 | DecodeToken(); 74 | 75 | var header = Encoding.UTF8.GetString(Convert.FromBase64String(_header)); 76 | var data = JsonConvert.DeserializeObject>(header); 77 | 78 | // Testing 79 | //foreach (var key in data) 80 | //{ 81 | // Console.WriteLine($"HEADER: {key.Key} = {key.Value}"); 82 | //} 83 | 84 | return data; 85 | } 86 | 87 | public string GetName() 88 | { 89 | string result = "Not Set"; 90 | string nameClaim = "sub"; 91 | 92 | Dictionary payload = GetPayload(); 93 | if (payload.ContainsKey(nameClaim)) 94 | { 95 | result = payload[nameClaim] as string; 96 | } 97 | 98 | return result; 99 | } 100 | 101 | public string GetFullName() 102 | { 103 | string result = "Not Set"; 104 | string givenNameClaim = "given_name"; 105 | 106 | Dictionary payload = GetPayload(); 107 | if (payload.ContainsKey(givenNameClaim)) 108 | { 109 | result = payload[givenNameClaim] as string; 110 | } 111 | 112 | return result; 113 | } 114 | 115 | public string GetEmail() 116 | { 117 | string result = "Not Set"; 118 | string emailClaim = "email"; 119 | 120 | Dictionary payload = GetPayload(); 121 | if (payload.ContainsKey(emailClaim)) 122 | { 123 | result = payload[emailClaim] as string; 124 | } 125 | 126 | return result; 127 | } 128 | 129 | public List GetRoles() 130 | { 131 | // DO NOT Change this claim name, ASP.NET Core roles auth requires this exact role for roles on a controller to work 132 | const string roleType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; 133 | 134 | List result = new List(); 135 | 136 | Dictionary payload = GetPayload(); 137 | if (payload.ContainsKey(roleType)) 138 | { 139 | // Role claims can either be a single string or a JArray (since where using Json.Net), we need to detect which 140 | if (payload[roleType] is JArray) 141 | { 142 | result.AddRange((payload[roleType] as JArray).ToObject>()); 143 | } 144 | 145 | if (payload[roleType] is string) 146 | { 147 | result.Add(payload[roleType] as string); 148 | } 149 | 150 | } 151 | 152 | return result; 153 | } 154 | 155 | public bool HasTokenExpired() 156 | { 157 | bool result = false; // Always default to NO 158 | string expiryClaim = "exp"; 159 | 160 | Dictionary payload = GetPayload(); 161 | if (payload.ContainsKey(expiryClaim)) 162 | { 163 | double timeStamp = Convert.ToDouble(payload[expiryClaim]); 164 | DateTime expiryTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); 165 | expiryTime = expiryTime.AddSeconds(timeStamp); 166 | if(expiryTime < DateTime.UtcNow) 167 | { 168 | result = true; 169 | } 170 | } 171 | 172 | return result; 173 | } 174 | 175 | public string GetIssuer() 176 | { 177 | string result = "NOT SET"; 178 | string issuerClaim = "iss"; 179 | 180 | Dictionary payload = GetPayload(); 181 | if (payload.ContainsKey(issuerClaim)) 182 | { 183 | result = payload[issuerClaim] as string; 184 | } 185 | 186 | return result; 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/Classes/ApplicationState.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Text; 3 | using Microsoft.JSInterop; 4 | using System.Threading.Tasks; 5 | using System.Net.Http.Headers; 6 | using System; 7 | using System.Collections.Generic; 8 | using Blazor.Extensions.Storage; 9 | using blazor.jwttest.Shared; 10 | using Microsoft.AspNetCore.Blazor.Services; 11 | using System.Linq; 12 | 13 | namespace blazor.jwttest.Client.Classes 14 | { 15 | public class ApplicationState 16 | { 17 | private readonly HttpClient _httpClient; 18 | private readonly LocalStorage _localStorage; 19 | private readonly JwtDecode _jwtDecoder; 20 | private readonly IUriHelper _uriHelper; 21 | 22 | private const string AuthTokenName = "authToken"; 23 | 24 | public event EventHandler LoginSucceeded; 25 | public event EventHandler LogoutSucceeded; 26 | 27 | public bool IsLoggedIn { get; private set; } 28 | public string UserName { get; private set; } 29 | public string FullName { get; private set; } 30 | public string Email { get; private set; } 31 | public List UserRoles { get; private set; } 32 | 33 | public ApplicationState( 34 | HttpClient httpClient, 35 | LocalStorage localStorage, 36 | JwtDecode jwtDecoder, 37 | IUriHelper uriHelper 38 | ) 39 | { 40 | _httpClient = httpClient; 41 | _localStorage = localStorage; 42 | _jwtDecoder = jwtDecoder; 43 | _uriHelper = uriHelper; 44 | UserName = String.Empty; 45 | UserRoles = new List(); 46 | } 47 | 48 | /// 49 | /// Contacts the backend API using the route /api/authentication/login and passes the provided LoginDetails to it 50 | /// if the login is successfull a JWT Token is returned and stored in the session store, a flag is set to show login 51 | /// was successfull, and things like the user name, roles etc are made available to be used in the app, followed by 52 | /// a LoginSuccessfull event being raised for any callers that require it 53 | /// 54 | /// LoginDetails class holding username & password to be used for authentication 55 | /// Async Task 56 | public async Task Login(LoginDetails loginDetails) 57 | { 58 | var response = await _httpClient.PostAsync("/api/authentication/login", 59 | new StringContent(Json.Serialize(loginDetails), 60 | Encoding.UTF8, 61 | "application/json")); 62 | 63 | if (response.IsSuccessStatusCode) 64 | { 65 | await SaveToken(response); 66 | await SetAuthorizationHeader(); 67 | await _jwtDecoder.LoadToken(AuthTokenName); 68 | 69 | UserName = _jwtDecoder.GetName(); 70 | FullName = _jwtDecoder.GetFullName(); 71 | Email = _jwtDecoder.GetEmail(); 72 | UserRoles = _jwtDecoder.GetRoles(); 73 | 74 | IsLoggedIn = true; 75 | 76 | LoginSucceeded?.Invoke(this, null); 77 | 78 | } 79 | } 80 | 81 | /// 82 | /// Removes any stored JWT Token, resets all the logged in user data then raises a LogoutSucceeded event to 83 | /// notify any event listners that need to know a logout has happened. 84 | /// 85 | /// Async Task 86 | public async Task Logout() 87 | { 88 | await _localStorage.RemoveItem(AuthTokenName); 89 | RemoveAuthorizationHeader(); 90 | 91 | IsLoggedIn = false; 92 | UserName = FullName = Email = String.Empty; 93 | UserRoles.Clear(); 94 | 95 | LogoutSucceeded?.Invoke(this, null); 96 | } 97 | 98 | /// 99 | /// Checks against any logged in user data it holds to determine if the current operation (Usually a page navigation) 100 | /// should be allowed to proceed. takes 2 required parameters and 1 optional parameter. 101 | /// 102 | /// List of roles that should be checked against user logged in roles to determine if user is allowed access 103 | /// out parameter of type NavigationFailReason, used to communicate reason for failiure back to caller 104 | /// [Optional] if provided, and user is not logged in, will attempt to redirect to the supplied route 105 | /// false if page navigation should be halted, otherwise true 106 | public bool IsAllowedToNavigate(in List allowedRoles, out NavigationFailReason failReason, in string notLoggedInRoute = "") 107 | { 108 | bool result = false; 109 | 110 | if (!IsLoggedIn) 111 | { 112 | if(!String.IsNullOrEmpty(notLoggedInRoute)) 113 | { 114 | _uriHelper.NavigateTo(notLoggedInRoute); 115 | } 116 | failReason = NavigationFailReason.NotLoggedIn; 117 | return result; 118 | } 119 | 120 | if (!UserRoles.Intersect(allowedRoles).Any()) 121 | { 122 | failReason = NavigationFailReason.RoleNotAllowed; 123 | return result; 124 | } 125 | 126 | result = true; 127 | failReason = NavigationFailReason.NoFail; 128 | return result; 129 | } 130 | 131 | private async Task SaveToken(HttpResponseMessage response) 132 | { 133 | var responseContent = await response.Content.ReadAsStringAsync(); 134 | var jwt = Json.Deserialize(responseContent); 135 | 136 | await _localStorage.SetItem(AuthTokenName, jwt.Token); 137 | } 138 | 139 | private async Task SetAuthorizationHeader() 140 | { 141 | if (!_httpClient.DefaultRequestHeaders.Contains("Authorization")) 142 | { 143 | var token = await _localStorage.GetItem(AuthTokenName); 144 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); 145 | } 146 | } 147 | 148 | private void RemoveAuthorizationHeader() 149 | { 150 | if (_httpClient.DefaultRequestHeaders.Contains("Authorization")) 151 | { 152 | _httpClient.DefaultRequestHeaders.Remove("Authorization"); 153 | } 154 | } 155 | 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | *.Development.json 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | **/Properties/launchSettings.json 57 | 58 | # StyleCop 59 | StyleCopReport.xml 60 | 61 | # Files built by Visual Studio 62 | *_i.c 63 | *_p.c 64 | *_i.h 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.iobj 69 | *.pch 70 | *.pdb 71 | *.ipdb 72 | *.pgc 73 | *.pgd 74 | *.rsp 75 | *.sbr 76 | *.tlb 77 | *.tli 78 | *.tlh 79 | *.tmp 80 | *.tmp_proj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush 296 | .cr/ 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /blazor.jwttest.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 9 | By P.J. Onori 10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 74 | 76 | 79 | 81 | 84 | 86 | 88 | 91 | 93 | 95 | 98 | 100 | 102 | 104 | 106 | 109 | 112 | 115 | 117 | 121 | 123 | 125 | 127 | 130 | 132 | 134 | 136 | 138 | 141 | 143 | 145 | 147 | 149 | 151 | 153 | 155 | 157 | 159 | 162 | 165 | 167 | 169 | 172 | 174 | 177 | 179 | 181 | 183 | 185 | 189 | 191 | 194 | 196 | 198 | 200 | 202 | 205 | 207 | 209 | 211 | 213 | 215 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 241 | 243 | 245 | 247 | 249 | 251 | 253 | 256 | 259 | 261 | 263 | 265 | 267 | 269 | 272 | 274 | 276 | 280 | 282 | 285 | 287 | 289 | 292 | 295 | 298 | 300 | 302 | 304 | 306 | 309 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 330 | 334 | 338 | 340 | 343 | 345 | 347 | 349 | 351 | 353 | 355 | 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 381 | 383 | 386 | 388 | 390 | 392 | 394 | 396 | 399 | 401 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 419 | 421 | 423 | 425 | 428 | 431 | 435 | 438 | 440 | 442 | 444 | 446 | 448 | 451 | 453 | 455 | 457 | 460 | 462 | 464 | 466 | 468 | 471 | 473 | 477 | 479 | 481 | 483 | 486 | 488 | 490 | 492 | 494 | 496 | 499 | 501 | 504 | 506 | 509 | 512 | 515 | 517 | 520 | 522 | 524 | 526 | 529 | 532 | 534 | 536 | 539 | 542 | 543 | 544 | --------------------------------------------------------------------------------