├── .editorconfig ├── .gitattributes ├── .gitignore ├── AuthzCustomMiddleware.sln ├── Directory.Build.props ├── LICENSE ├── README.md ├── docker-compose.yml ├── global.json ├── src ├── API │ ├── API.csproj │ ├── Authorization │ │ ├── PermissionsMiddleware.cs │ │ └── UserPermissionService.cs │ ├── Configuration │ │ └── AppSettings.cs │ ├── Controllers │ │ ├── Models │ │ │ └── UserClaimsResponse.cs │ │ ├── ProductsController.cs │ │ └── UserController.cs │ ├── EF │ │ ├── AuthzContext.cs │ │ ├── DbConfiguration │ │ │ ├── PermissionEntityConfiguration.cs │ │ │ ├── UserEntityConfiguration.cs │ │ │ └── UserPermissionEntityConfiguration.cs │ │ ├── Permission.cs │ │ ├── User.cs │ │ └── UserPermission.cs │ ├── Infrastructure │ │ ├── DbMigratorHostedService.cs │ │ ├── ServiceCollectionExtensions.cs │ │ └── SwaggerAuthorizeOperationFilter.cs │ ├── Migrations │ │ ├── 20210130112155_Initial.Designer.cs │ │ ├── 20210130112155_Initial.cs │ │ ├── 20210324213434_ClearExistingData.Designer.cs │ │ ├── 20210324213434_ClearExistingData.cs │ │ └── AuthzContextModelSnapshot.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── AuthUtils │ ├── AppClaimTypes.cs │ ├── AuthUtils.csproj │ ├── HttpContextExtensions.cs │ ├── Permissions.cs │ ├── PolicyProvider │ │ ├── PermissionAuthorizationPolicyProvider.cs │ │ ├── PermissionAuthorizeAttribute.cs │ │ ├── PermissionHandler.cs │ │ └── PermissionRequirement.cs │ └── StandardJwtClaimTypes.cs └── IdentityServer │ ├── .dockerignore │ ├── Config.cs │ ├── Dockerfile │ ├── IdentityServer.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Quickstart │ ├── Account │ │ ├── AccountController.cs │ │ ├── AccountOptions.cs │ │ ├── ExternalController.cs │ │ ├── ExternalProvider.cs │ │ ├── LoggedOutViewModel.cs │ │ ├── LoginInputModel.cs │ │ ├── LoginViewModel.cs │ │ ├── LogoutInputModel.cs │ │ ├── LogoutViewModel.cs │ │ └── RedirectViewModel.cs │ ├── Consent │ │ ├── ConsentController.cs │ │ ├── ConsentInputModel.cs │ │ ├── ConsentOptions.cs │ │ ├── ConsentViewModel.cs │ │ ├── ProcessConsentResult.cs │ │ └── ScopeViewModel.cs │ ├── Device │ │ ├── DeviceAuthorizationInputModel.cs │ │ ├── DeviceAuthorizationViewModel.cs │ │ └── DeviceController.cs │ ├── Diagnostics │ │ ├── DiagnosticsController.cs │ │ └── DiagnosticsViewModel.cs │ ├── Extensions.cs │ ├── Grants │ │ ├── GrantsController.cs │ │ └── GrantsViewModel.cs │ ├── Home │ │ ├── ErrorViewModel.cs │ │ └── HomeController.cs │ ├── SecurityHeadersAttribute.cs │ └── TestUsers.cs │ ├── Startup.cs │ ├── Views │ ├── Account │ │ ├── AccessDenied.cshtml │ │ ├── LoggedOut.cshtml │ │ ├── Login.cshtml │ │ └── Logout.cshtml │ ├── Consent │ │ └── Index.cshtml │ ├── Device │ │ ├── Success.cshtml │ │ ├── UserCodeCapture.cshtml │ │ └── UserCodeConfirmation.cshtml │ ├── Diagnostics │ │ └── Index.cshtml │ ├── Grants │ │ └── Index.cshtml │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── Redirect.cshtml │ │ ├── _Layout.cshtml │ │ ├── _Nav.cshtml │ │ ├── _ScopeListItem.cshtml │ │ └── _ValidationSummary.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── tempkey.jwk │ ├── updateUI.ps1 │ └── wwwroot │ ├── css │ ├── site.css │ ├── site.min.css │ └── site.scss │ ├── favicon.ico │ ├── icon.jpg │ ├── icon.png │ ├── js │ ├── signin-redirect.js │ └── signout-redirect.js │ └── lib │ ├── bootstrap │ ├── README.md │ ├── dist │ │ ├── css │ │ │ ├── bootstrap-grid.css │ │ │ ├── bootstrap-grid.css.map │ │ │ ├── bootstrap-grid.min.css │ │ │ ├── bootstrap-grid.min.css.map │ │ │ ├── bootstrap-reboot.css │ │ │ ├── bootstrap-reboot.css.map │ │ │ ├── bootstrap-reboot.min.css │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ └── js │ │ │ ├── bootstrap.bundle.js │ │ │ ├── bootstrap.bundle.js.map │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ └── scss │ │ ├── _alert.scss │ │ ├── _badge.scss │ │ ├── _breadcrumb.scss │ │ ├── _button-group.scss │ │ ├── _buttons.scss │ │ ├── _card.scss │ │ ├── _carousel.scss │ │ ├── _close.scss │ │ ├── _code.scss │ │ ├── _custom-forms.scss │ │ ├── _dropdown.scss │ │ ├── _forms.scss │ │ ├── _functions.scss │ │ ├── _grid.scss │ │ ├── _images.scss │ │ ├── _input-group.scss │ │ ├── _jumbotron.scss │ │ ├── _list-group.scss │ │ ├── _media.scss │ │ ├── _mixins.scss │ │ ├── _modal.scss │ │ ├── _nav.scss │ │ ├── _navbar.scss │ │ ├── _pagination.scss │ │ ├── _popover.scss │ │ ├── _print.scss │ │ ├── _progress.scss │ │ ├── _reboot.scss │ │ ├── _root.scss │ │ ├── _spinners.scss │ │ ├── _tables.scss │ │ ├── _toasts.scss │ │ ├── _tooltip.scss │ │ ├── _transitions.scss │ │ ├── _type.scss │ │ ├── _utilities.scss │ │ ├── _variables.scss │ │ ├── bootstrap-grid.scss │ │ ├── bootstrap-reboot.scss │ │ ├── bootstrap.scss │ │ ├── mixins │ │ ├── _alert.scss │ │ ├── _background-variant.scss │ │ ├── _badge.scss │ │ ├── _border-radius.scss │ │ ├── _box-shadow.scss │ │ ├── _breakpoints.scss │ │ ├── _buttons.scss │ │ ├── _caret.scss │ │ ├── _clearfix.scss │ │ ├── _deprecate.scss │ │ ├── _float.scss │ │ ├── _forms.scss │ │ ├── _gradients.scss │ │ ├── _grid-framework.scss │ │ ├── _grid.scss │ │ ├── _hover.scss │ │ ├── _image.scss │ │ ├── _list-group.scss │ │ ├── _lists.scss │ │ ├── _nav-divider.scss │ │ ├── _pagination.scss │ │ ├── _reset-text.scss │ │ ├── _resize.scss │ │ ├── _screen-reader.scss │ │ ├── _size.scss │ │ ├── _table-row.scss │ │ ├── _text-emphasis.scss │ │ ├── _text-hide.scss │ │ ├── _text-truncate.scss │ │ ├── _transition.scss │ │ └── _visibility.scss │ │ ├── utilities │ │ ├── _align.scss │ │ ├── _background.scss │ │ ├── _borders.scss │ │ ├── _clearfix.scss │ │ ├── _display.scss │ │ ├── _embed.scss │ │ ├── _flex.scss │ │ ├── _float.scss │ │ ├── _overflow.scss │ │ ├── _position.scss │ │ ├── _screenreaders.scss │ │ ├── _shadows.scss │ │ ├── _sizing.scss │ │ ├── _spacing.scss │ │ ├── _stretched-link.scss │ │ ├── _text.scss │ │ └── _visibility.scss │ │ └── vendor │ │ └── _rfs.scss │ └── jquery │ ├── LICENSE.txt │ ├── README.md │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ ├── jquery.min.map │ ├── jquery.slim.js │ ├── jquery.slim.min.js │ └── jquery.slim.min.map └── tests └── API.Tests ├── API.Tests.csproj ├── ApiApplicationFactory.cs ├── Controllers ├── ProductControllerTests.cs └── UserControllerTests.cs └── MockAuth ├── AuthConstants.cs ├── AuthServiceCollectionExtensions.cs ├── MockAuthUser.cs └── TestAuthHandler.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto encoding=UTF-8 5 | 6 | # csc/vbc are shell scripts and should always have unix line endings 7 | # These shell scripts are included in the toolset packages. Normally, the shell 8 | # scripts in our repo are only run by cloning onto a Linux/Mac machine, and git 9 | # automatically chooses LF as the line ending. 10 | # 11 | # However, right now the toolset packages must be built on Windows, and so the 12 | # files must be hard-coded to be cloned with LF 13 | src/Compilers/CSharp/CscCore/csc text eol=lf 14 | src/Compilers/VisualBasic/VbcCore/vbc text eol=lf 15 | 16 | ############################################################################### 17 | # Set default behavior for command prompt diff. 18 | # 19 | # This is need for earlier builds of msysgit that does not have it on by 20 | # default for csharp files. 21 | # Note: This is only used by command line 22 | ############################################################################### 23 | *.cs diff=csharp text 24 | *.vb text 25 | 26 | ############################################################################### 27 | # Set the merge driver for project and solution files 28 | # 29 | # Merging from the command prompt will add diff markers to the files if there 30 | # are conflicts (Merging from VS is not affected by the settings below, in VS 31 | # the diff markers are never inserted). Diff markers may cause the following 32 | # file extensions to fail to load in VS. An alternative would be to treat 33 | # these files as binary and thus will always conflict and require user 34 | # intervention with every merge. To do so, just uncomment the entries below 35 | ############################################################################### 36 | #*.sln merge=binary 37 | #*.csproj merge=binary 38 | #*.vbproj merge=binary 39 | #*.vcxproj merge=binary 40 | #*.vcproj merge=binary 41 | #*.dbproj merge=binary 42 | #*.fsproj merge=binary 43 | #*.lsproj merge=binary 44 | #*.wixproj merge=binary 45 | #*.modelproj merge=binary 46 | #*.sqlproj merge=binary 47 | #*.wwaproj merge=binary 48 | 49 | ############################################################################### 50 | # behavior for image files 51 | # 52 | # image files are treated as binary by default. 53 | ############################################################################### 54 | #*.jpg binary 55 | #*.png binary 56 | #*.gif binary 57 | 58 | ############################################################################### 59 | # diff behavior for common document formats 60 | # 61 | # Convert binary document formats to text before diffing them. This feature 62 | # is only available from the command line. Turn it on by uncommenting the 63 | # entries below. 64 | ############################################################################### 65 | #*.doc diff=astextplain 66 | #*.DOC diff=astextplain 67 | #*.docx diff=astextplain 68 | #*.DOCX diff=astextplain 69 | #*.dot diff=astextplain 70 | #*.DOT diff=astextplain 71 | #*.pdf diff=astextplain 72 | #*.PDF diff=astextplain 73 | #*.rtf diff=astextplain 74 | #*.RTF diff=astextplain 75 | -------------------------------------------------------------------------------- /AuthzCustomMiddleware.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # 4 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0A4E7730-9810-4C26-9B09-B241A9E99699}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityServer", "src\IdentityServer\IdentityServer.csproj", "{97D8A0C9-A7AF-452D-9225-587095803E59}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API", "src\API\API.csproj", "{676E5BA0-D1B1-4E1D-A952-3234EA27FFE1}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthUtils", "src\AuthUtils\AuthUtils.csproj", "{FE7AD4A7-DA39-4A6A-AFF8-D6091500D415}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E13221C1-9983-4B51-BCE0-4E4533E90FDF}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "API.Tests", "tests\API.Tests\API.Tests.csproj", "{90857CE7-E7B6-4080-B6D3-ED26AAAF9515}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {97D8A0C9-A7AF-452D-9225-587095803E59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {97D8A0C9-A7AF-452D-9225-587095803E59}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {97D8A0C9-A7AF-452D-9225-587095803E59}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {97D8A0C9-A7AF-452D-9225-587095803E59}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {676E5BA0-D1B1-4E1D-A952-3234EA27FFE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {676E5BA0-D1B1-4E1D-A952-3234EA27FFE1}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {676E5BA0-D1B1-4E1D-A952-3234EA27FFE1}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {676E5BA0-D1B1-4E1D-A952-3234EA27FFE1}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {FE7AD4A7-DA39-4A6A-AFF8-D6091500D415}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {FE7AD4A7-DA39-4A6A-AFF8-D6091500D415}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {FE7AD4A7-DA39-4A6A-AFF8-D6091500D415}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {FE7AD4A7-DA39-4A6A-AFF8-D6091500D415}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {90857CE7-E7B6-4080-B6D3-ED26AAAF9515}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {90857CE7-E7B6-4080-B6D3-ED26AAAF9515}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {90857CE7-E7B6-4080-B6D3-ED26AAAF9515}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {90857CE7-E7B6-4080-B6D3-ED26AAAF9515}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(NestedProjects) = preSolution 40 | {97D8A0C9-A7AF-452D-9225-587095803E59} = {0A4E7730-9810-4C26-9B09-B241A9E99699} 41 | {676E5BA0-D1B1-4E1D-A952-3234EA27FFE1} = {0A4E7730-9810-4C26-9B09-B241A9E99699} 42 | {FE7AD4A7-DA39-4A6A-AFF8-D6091500D415} = {0A4E7730-9810-4C26-9B09-B241A9E99699} 43 | {90857CE7-E7B6-4080-B6D3-ED26AAAF9515} = {E13221C1-9983-4B51-BCE0-4E4533E90FDF} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | enable 5 | true 6 | $(NoWarn);1591 7 | 8 | 9 | 10 | 11 | 12 | <_Parameter1>$(MSBuildProjectName).Tests 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joao Grassi 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authorization in ASP.NET Core 2 | 3 | This repo contains the source code for a series of blog posts I'm writing about authorization in ASP.NET Core. You can check all the posts here: [Authorization in ASP.NET Core - Series](https://blog.joaograssi.com/series/authorization-in-asp.net-core). 4 | 5 | 6 | ## Blog posts and code - how to follow 7 | 8 | Since there's a lot to cover, this repo is going to be updated incrementally, together with each post. 9 | 10 | To make it practical for you to both read the post and follow through with the code, there will be a branch for each post I publish. This way you can see the *snapshot* of how it looked like when I published the post. 11 | 12 | The `main` branch will contain the latest state always. 13 | 14 | ## Running the app: 15 | 16 | The app uses a SQL Server database to store its data. At the root of the repo, you can find a `docker-compose.yml` which will start both SQL Server and IdentityServer for you. 17 | 18 | After executing `docker-compose up`, you can start debugging the API using your preferred IDE. The `API` project will automatically create and migrate the database using EF Migrations. 19 | 20 | The initial migration will seed the database with users and permissions so you don't have to do anything to get started. 21 | 22 | The users you can use to login are: (If you ever used IdentityServer you might be familiar with them :smile:). 23 | 24 | | User | Password | 25 | |------|----------| 26 | | bob | bob | 27 | | alice | alice | 28 | 29 | 30 | If all worked you should see the Swagger page and should be able to authenticate and call the endpoint. 31 | 32 | ### Requisites 33 | 34 | - .NET SDK 6.* 35 | - Docker/Compose (only if you want to run the app locally) 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | networks: 4 | authz-custom-network: 5 | 6 | services: 7 | authz-custom-sqlserver: 8 | image: "mcr.microsoft.com/azure-sql-edge" 9 | ports: 10 | - "1433:1433" 11 | environment: 12 | SA_PASSWORD: "2@LaiNw)PDvs^t>L!Ybt]6H^%h3U>M" 13 | ACCEPT_EULA: "Y" 14 | networks: 15 | - authz-custom-network 16 | authz-idsrv: 17 | build: 18 | context: ./src/IdentityServer 19 | dockerfile: Dockerfile 20 | ports: 21 | - "5002:80" 22 | networks: 23 | - authz-custom-network 24 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.101", 4 | "rollForward": "latestFeature" 5 | } 6 | } -------------------------------------------------------------------------------- /src/API/API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/API/Authorization/PermissionsMiddleware.cs: -------------------------------------------------------------------------------- 1 | using AuthUtils; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Logging; 4 | using System.Threading.Tasks; 5 | 6 | namespace API.Authorization 7 | { 8 | public class PermissionsMiddleware 9 | { 10 | private readonly RequestDelegate _next; 11 | private readonly ILogger _logger; 12 | 13 | public PermissionsMiddleware( 14 | RequestDelegate next, 15 | ILogger logger) 16 | { 17 | _next = next; 18 | _logger = logger; 19 | } 20 | 21 | public async Task InvokeAsync( 22 | HttpContext context, IUserPermissionService permissionService) 23 | { 24 | if (context.User.Identity == null || !context.User.Identity.IsAuthenticated) 25 | { 26 | await _next(context); 27 | return; 28 | } 29 | 30 | var cancellationToken = context.RequestAborted; 31 | 32 | var userSub = context.User.FindFirst(StandardJwtClaimTypes.Subject)?.Value; 33 | if (string.IsNullOrEmpty(userSub)) 34 | { 35 | await context.WriteAccessDeniedResponse("User 'sub' claim is required", cancellationToken: cancellationToken); 36 | return; 37 | } 38 | 39 | var permissionsIdentity = await permissionService.GetUserPermissionsIdentity(userSub, cancellationToken); 40 | if (permissionsIdentity == null) 41 | { 42 | _logger.LogWarning("User {sub} does not have permissions", userSub); 43 | 44 | await context.WriteAccessDeniedResponse(cancellationToken: cancellationToken); 45 | return; 46 | } 47 | 48 | // User has permissions, so we add the extra identity containing the "permissions" claims 49 | context.User.AddIdentity(permissionsIdentity); 50 | await _next(context); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/API/Authorization/UserPermissionService.cs: -------------------------------------------------------------------------------- 1 | // unset 2 | 3 | using API.EF; 4 | using AuthUtils; 5 | using Microsoft.EntityFrameworkCore; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Security.Claims; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace API.Authorization 13 | { 14 | public interface IUserPermissionService 15 | { 16 | /// 17 | /// Returns a new identity containing the user permissions as Claims 18 | /// 19 | /// The user external id (sub claim) 20 | /// 21 | ValueTask GetUserPermissionsIdentity(string sub, CancellationToken cancellationToken); 22 | } 23 | 24 | public class UserPermissionService : IUserPermissionService 25 | { 26 | private readonly AuthzContext _dbContext; 27 | 28 | public UserPermissionService(AuthzContext dbContext) 29 | { 30 | _dbContext = dbContext; 31 | } 32 | 33 | public async ValueTask GetUserPermissionsIdentity( 34 | string sub, CancellationToken cancellationToken) 35 | { 36 | var userPermissions = await 37 | (from up in _dbContext.UserPermissions 38 | join perm in _dbContext.Permissions on up.PermissionId equals perm.Id 39 | join user in _dbContext.Users on up.UserId equals user.Id 40 | where user.ExternalId == sub 41 | select new Claim(AppClaimTypes.Permissions, perm.Name)).ToListAsync(cancellationToken); 42 | 43 | return CreatePermissionsIdentity(userPermissions); 44 | } 45 | 46 | private static ClaimsIdentity? CreatePermissionsIdentity(IReadOnlyCollection claimPermissions) 47 | { 48 | if (!claimPermissions.Any()) 49 | return null; 50 | 51 | var permissionsIdentity = new ClaimsIdentity(nameof(PermissionsMiddleware), "name", "role"); 52 | permissionsIdentity.AddClaims(claimPermissions); 53 | 54 | return permissionsIdentity; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/API/Configuration/AppSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace API.Configuration 4 | { 5 | public class AppSettings 6 | { 7 | internal const string SectionName = "AppSettings"; 8 | 9 | public SwaggerSettings Swagger { get; set; } = null!; 10 | 11 | public IdentityServerSettings IdentityServer { get; set; } = null!; 12 | } 13 | 14 | public class SwaggerSettings 15 | { 16 | public string ClientId { get; set; } = null!; 17 | 18 | public string Title { get; set; } = null!; 19 | 20 | public Dictionary Scopes { get; set; } = new(); 21 | } 22 | 23 | public class IdentityServerSettings 24 | { 25 | public string BaseUrl { get; set; } = null!; 26 | 27 | public string Audience { get; set; } = null!; 28 | 29 | public bool RequireHttps { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /src/API/Controllers/Models/UserClaimsResponse.cs: -------------------------------------------------------------------------------- 1 | namespace API.Controllers.Models 2 | { 3 | public class UserClaimsResponse 4 | { 5 | public string Type { get; set; } = null!; 6 | 7 | public string Value { get; set; } = null!; 8 | 9 | public UserClaimsResponse() { } 10 | 11 | public UserClaimsResponse(string type, string value) 12 | { 13 | Type = type; 14 | Value = value; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/API/Controllers/ProductsController.cs: -------------------------------------------------------------------------------- 1 | using AuthUtils; 2 | using AuthUtils.PolicyProvider; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace API.Controllers 7 | { 8 | [Authorize] 9 | [ApiController] 10 | [Route("products")] 11 | public class ProductsController : ControllerBase 12 | { 13 | /// 14 | /// Both Alice and Bob can see access this. Only 'Read' is required 15 | /// 16 | [PermissionAuthorize(Permissions.Read)] 17 | [HttpGet] 18 | public IActionResult Get() 19 | { 20 | return Ok("We've got products!"); 21 | } 22 | 23 | /// 24 | /// Only Alice can see access this. Only 'Create' is required 25 | /// 26 | [PermissionAuthorize(Permissions.Create)] 27 | [HttpPost] 28 | public IActionResult Create() 29 | { 30 | return Ok("I'm such a creator!"); 31 | } 32 | 33 | /// 34 | /// Only Alice can see access this. 'Update' AND 'Read' are required 35 | /// 36 | [PermissionAuthorize(PermissionOperator.And, Permissions.Update, Permissions.Read)] 37 | [HttpPut] 38 | public IActionResult Update() 39 | { 40 | return Ok("It's good to change things sometimes!"); 41 | } 42 | 43 | /// 44 | /// Both Alice and Bob can see access this. 'Delete' OR 'Read' are required 45 | /// 46 | /// 47 | /// Don't @ me please. Users with 'Read' permission shouldn't be able to delete stuff, I know. 48 | /// This is just to demonstrate the OR operator. 49 | /// 50 | [PermissionAuthorize(PermissionOperator.Or, Permissions.Delete, Permissions.Read)] 51 | [HttpDelete] 52 | public IActionResult Delete() 53 | { 54 | return Ok("Aaaaaaaaand I'm gone."); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/API/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using API.Controllers.Models; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | using static Microsoft.AspNetCore.Http.StatusCodes; 8 | 9 | namespace API.Controllers 10 | { 11 | [Authorize] 12 | [ApiController] 13 | [Route("users")] 14 | public class UserController : ControllerBase 15 | { 16 | /// 17 | /// Call this endpoint to see all the logged-in user claims! 18 | /// 19 | [HttpGet("me")] 20 | [ProducesResponseType(typeof(List), Status200OK)] 21 | public IActionResult GetUserClaims() 22 | { 23 | return Ok(User.Claims.Select(c => new UserClaimsResponse(c.Type, c.Value))); 24 | } 25 | 26 | /// 27 | /// only alice can access this. Try! 28 | /// 29 | [HttpGet("secret")] 30 | [Authorize(Roles = "Manager")] 31 | public IActionResult GetSecretData() 32 | { 33 | return Ok("This is secret data - For managers only!"); 34 | } 35 | 36 | /// 37 | /// Only bob can buy alcoholic drinks! 38 | /// 39 | [HttpGet("cannot-buy-this")] 40 | [Authorize(Policy = "Over18YearsOld")] 41 | public IActionResult GetAlhocolicBeverage() 42 | { 43 | return Ok("Bob is enjoying some whisky now!"); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/API/EF/AuthzContext.cs: -------------------------------------------------------------------------------- 1 | // unset 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace API.EF 6 | { 7 | public class AuthzContext : DbContext 8 | { 9 | public DbSet Users { get; set; } = null!; 10 | public DbSet Permissions { get; set; } = null!; 11 | public DbSet UserPermissions { get; set; } = null!; 12 | 13 | public AuthzContext(DbContextOptions options) : base(options) 14 | { 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder builder) 18 | { 19 | // Applies all the configurations for entities. See the Configuration folder 20 | builder.ApplyConfigurationsFromAssembly(GetType().Assembly); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/API/EF/DbConfiguration/PermissionEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | // unset 2 | 3 | using AuthUtils; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | namespace API.EF.DbConfiguration 10 | { 11 | public class PermissionEntityConfiguration : IEntityTypeConfiguration 12 | { 13 | public void Configure(EntityTypeBuilder builder) 14 | { 15 | builder.HasKey(p => p.Id); 16 | builder.Property(p => p.Id).ValueGeneratedNever(); 17 | 18 | builder.Property(p => p.Name).HasMaxLength(255); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/API/EF/DbConfiguration/UserEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | // unset 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace API.EF.DbConfiguration 9 | { 10 | public class UserEntityConfiguration : IEntityTypeConfiguration 11 | { 12 | public void Configure(EntityTypeBuilder builder) 13 | { 14 | builder.HasKey(p => p.Id); 15 | builder.Property(p => p.Id).ValueGeneratedNever(); 16 | 17 | builder.Property(p => p.ExternalId).HasMaxLength(255); 18 | builder.Property(p => p.Email).HasMaxLength(255); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/API/EF/DbConfiguration/UserPermissionEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | 4 | namespace API.EF.DbConfiguration 5 | { 6 | public class UserPermissionEntityConfiguration : IEntityTypeConfiguration 7 | { 8 | public void Configure(EntityTypeBuilder builder) 9 | { 10 | builder.HasKey(p => p.Id); 11 | builder.Property(p => p.Id).ValueGeneratedNever(); 12 | 13 | builder.HasOne(p => p.User) 14 | .WithMany(p => p.Permissions) 15 | .HasForeignKey(pt => pt.UserId); 16 | 17 | builder.HasOne(p => p.Permission) 18 | .WithMany() 19 | .HasForeignKey(pt => pt.PermissionId); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/API/EF/Permission.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace API.EF 5 | { 6 | public class Permission 7 | { 8 | public Guid Id { get; private set; } 9 | 10 | public string Name { get; private set; } 11 | 12 | public Permission(Guid id, string name) 13 | { 14 | Id = id; 15 | Name = name; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/API/EF/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace API.EF 5 | { 6 | public class User 7 | { 8 | /// 9 | /// Our internal Id 10 | /// 11 | public Guid Id { get; private set; } 12 | 13 | /// 14 | /// The sub claim from the JWT token 15 | /// 16 | public string ExternalId { get; private set; } 17 | 18 | public string Email { get; private set; } 19 | 20 | public List Permissions { get; private set; } = new(); 21 | 22 | public User(Guid id, string externalId, string email) 23 | { 24 | Id = id; 25 | ExternalId = externalId; 26 | Email = email; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/API/EF/UserPermission.cs: -------------------------------------------------------------------------------- 1 | // unset 2 | 3 | using System; 4 | 5 | namespace API.EF 6 | { 7 | public class UserPermission 8 | { 9 | private User? _user; 10 | private Permission? _permission; 11 | 12 | public Guid Id { get; private set; } 13 | 14 | public Guid UserId { get; private set; } 15 | 16 | public User User 17 | { 18 | set => _user = value; 19 | get => _user 20 | ?? throw new InvalidOperationException("Uninitialized property: " + nameof(User)); 21 | } 22 | 23 | public Guid PermissionId { get; private set; } 24 | 25 | public Permission Permission 26 | { 27 | set => _permission = value; 28 | get => _permission 29 | ?? throw new InvalidOperationException("Uninitialized property: " + nameof(Permission)); 30 | } 31 | 32 | public UserPermission(Guid id, Guid userId, Guid permissionId) 33 | { 34 | Id = id; 35 | UserId = userId; 36 | PermissionId = permissionId; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/API/Infrastructure/DbMigratorHostedService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using API.EF; 6 | using AuthUtils; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace API.Infrastructure 13 | { 14 | /// 15 | /// A hosted service that will migrate the database automatically. 16 | /// This is only for demo/dev purpose.. 17 | /// 18 | public class DbMigratorHostedService : IHostedService 19 | { 20 | private readonly IServiceProvider _serviceProvider; 21 | private readonly ILogger _logger; 22 | 23 | public DbMigratorHostedService(IServiceProvider services, 24 | ILogger logger) 25 | { 26 | _serviceProvider = services; 27 | _logger = logger; 28 | } 29 | 30 | public async Task StartAsync(CancellationToken cancellationToken) 31 | { 32 | using var scope = _serviceProvider.CreateScope(); 33 | var dbContext = scope.ServiceProvider.GetRequiredService(); 34 | await dbContext.Database.MigrateAsync(cancellationToken); 35 | 36 | await SeedDb(dbContext, cancellationToken); 37 | } 38 | 39 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 40 | 41 | private async ValueTask SeedDb(AuthzContext dbContext, CancellationToken cancellationToken) 42 | { 43 | if (!await dbContext.Permissions.AnyAsync(cancellationToken)) 44 | { 45 | // if no permissions are present, we create everything from scratch 46 | // this is so we can all be in the same page during the blog post; 47 | 48 | var permissions = new[] 49 | { 50 | new Permission(Guid.NewGuid(), Permissions.Create), 51 | new Permission(Guid.NewGuid(), Permissions.Read), 52 | new Permission(Guid.NewGuid(), Permissions.Update), 53 | new Permission(Guid.NewGuid(), Permissions.Delete) 54 | }; 55 | dbContext.Permissions.AddRange(permissions); 56 | 57 | var users = new[] 58 | { 59 | new User(Guid.NewGuid(), "818727", "alicesmith@email.com"), 60 | new User(Guid.NewGuid(), "88421113", "bobsmith@email.com") 61 | }; 62 | dbContext.Users.AddRange(users); 63 | 64 | var alicePermissions = new[] 65 | { 66 | // Alice has CRUD permissions 67 | new UserPermission(Guid.NewGuid(), users[0].Id, permissions[0].Id), 68 | new UserPermission(Guid.NewGuid(), users[0].Id, permissions[1].Id), 69 | new UserPermission(Guid.NewGuid(), users[0].Id, permissions[2].Id), 70 | new UserPermission(Guid.NewGuid(), users[0].Id, permissions[3].Id) 71 | }; 72 | 73 | var bobPermissions = new[] 74 | { 75 | // Bob has only Read permission 76 | new UserPermission(Guid.NewGuid(), users[1].Id, permissions[1].Id), 77 | }; 78 | dbContext.UserPermissions.AddRange(alicePermissions.Concat(bobPermissions)); 79 | 80 | await dbContext.SaveChangesAsync(cancellationToken); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/API/Infrastructure/SwaggerAuthorizeOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.OpenApi.Models; 5 | using Swashbuckle.AspNetCore.SwaggerGen; 6 | 7 | namespace API.Infrastructure 8 | { 9 | public class SwaggerAuthorizeOperationFilter : IOperationFilter 10 | { 11 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 12 | { 13 | var hasAuthorize = 14 | context.MethodInfo.DeclaringType!.GetCustomAttributes(true).OfType().Any() 15 | || context.MethodInfo.GetCustomAttributes(true).OfType().Any(); 16 | 17 | if (!hasAuthorize) 18 | return; 19 | 20 | operation.Responses.Add("401", new OpenApiResponse {Description = "Unauthorized"}); 21 | operation.Responses.Add("403", new OpenApiResponse {Description = "Forbidden"}); 22 | 23 | var oAuthScheme = new OpenApiSecurityScheme 24 | { 25 | Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } 26 | }; 27 | 28 | operation.Security = new List 29 | { 30 | new OpenApiSecurityRequirement 31 | { 32 | [ oAuthScheme ] = new[] {"api"} 33 | } 34 | }; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/API/Migrations/20210324213434_ClearExistingData.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using API.EF; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace API.Migrations 11 | { 12 | [DbContext(typeof(AuthzContext))] 13 | [Migration("20210324213434_ClearExistingData")] 14 | partial class ClearExistingData 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .UseIdentityColumns() 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("ProductVersion", "5.0.2"); 23 | 24 | modelBuilder.Entity("API.EF.Permission", b => 25 | { 26 | b.Property("Id") 27 | .HasColumnType("uniqueidentifier"); 28 | 29 | b.Property("Name") 30 | .IsRequired() 31 | .HasMaxLength(255) 32 | .HasColumnType("nvarchar(255)"); 33 | 34 | b.HasKey("Id"); 35 | 36 | b.ToTable("Permissions"); 37 | }); 38 | 39 | modelBuilder.Entity("API.EF.User", b => 40 | { 41 | b.Property("Id") 42 | .HasColumnType("uniqueidentifier"); 43 | 44 | b.Property("Email") 45 | .IsRequired() 46 | .HasMaxLength(255) 47 | .HasColumnType("nvarchar(255)"); 48 | 49 | b.Property("ExternalId") 50 | .IsRequired() 51 | .HasMaxLength(255) 52 | .HasColumnType("nvarchar(255)"); 53 | 54 | b.HasKey("Id"); 55 | 56 | b.ToTable("Users"); 57 | }); 58 | 59 | modelBuilder.Entity("API.EF.UserPermission", b => 60 | { 61 | b.Property("Id") 62 | .HasColumnType("uniqueidentifier"); 63 | 64 | b.Property("PermissionId") 65 | .HasColumnType("uniqueidentifier"); 66 | 67 | b.Property("UserId") 68 | .HasColumnType("uniqueidentifier"); 69 | 70 | b.HasKey("Id"); 71 | 72 | b.HasIndex("PermissionId"); 73 | 74 | b.HasIndex("UserId"); 75 | 76 | b.ToTable("UserPermissions"); 77 | }); 78 | 79 | modelBuilder.Entity("API.EF.UserPermission", b => 80 | { 81 | b.HasOne("API.EF.Permission", "Permission") 82 | .WithMany() 83 | .HasForeignKey("PermissionId") 84 | .OnDelete(DeleteBehavior.Cascade) 85 | .IsRequired(); 86 | 87 | b.HasOne("API.EF.User", "User") 88 | .WithMany("Permissions") 89 | .HasForeignKey("UserId") 90 | .OnDelete(DeleteBehavior.Cascade) 91 | .IsRequired(); 92 | 93 | b.Navigation("Permission"); 94 | 95 | b.Navigation("User"); 96 | }); 97 | 98 | modelBuilder.Entity("API.EF.User", b => 99 | { 100 | b.Navigation("Permissions"); 101 | }); 102 | #pragma warning restore 612, 618 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/API/Migrations/20210324213434_ClearExistingData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace API.Migrations 5 | { 6 | public partial class ClearExistingData : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | // deleted previous inserted data; New data is inserted via DbMigratorHostedService.cs 11 | migrationBuilder.Sql("DELETE FROM UserPermissions"); 12 | migrationBuilder.Sql("DELETE FROM Users"); 13 | migrationBuilder.Sql("DELETE FROM Permissions"); 14 | } 15 | 16 | protected override void Down(MigrationBuilder migrationBuilder) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/API/Migrations/AuthzContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using API.EF; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace API.Migrations 10 | { 11 | [DbContext(typeof(AuthzContext))] 12 | partial class AuthzContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .UseIdentityColumns() 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("ProductVersion", "5.0.2"); 21 | 22 | modelBuilder.Entity("API.EF.Permission", b => 23 | { 24 | b.Property("Id") 25 | .HasColumnType("uniqueidentifier"); 26 | 27 | b.Property("Name") 28 | .IsRequired() 29 | .HasMaxLength(255) 30 | .HasColumnType("nvarchar(255)"); 31 | 32 | b.HasKey("Id"); 33 | 34 | b.ToTable("Permissions"); 35 | }); 36 | 37 | modelBuilder.Entity("API.EF.User", b => 38 | { 39 | b.Property("Id") 40 | .HasColumnType("uniqueidentifier"); 41 | 42 | b.Property("Email") 43 | .IsRequired() 44 | .HasMaxLength(255) 45 | .HasColumnType("nvarchar(255)"); 46 | 47 | b.Property("ExternalId") 48 | .IsRequired() 49 | .HasMaxLength(255) 50 | .HasColumnType("nvarchar(255)"); 51 | 52 | b.HasKey("Id"); 53 | 54 | b.ToTable("Users"); 55 | }); 56 | 57 | modelBuilder.Entity("API.EF.UserPermission", b => 58 | { 59 | b.Property("Id") 60 | .HasColumnType("uniqueidentifier"); 61 | 62 | b.Property("PermissionId") 63 | .HasColumnType("uniqueidentifier"); 64 | 65 | b.Property("UserId") 66 | .HasColumnType("uniqueidentifier"); 67 | 68 | b.HasKey("Id"); 69 | 70 | b.HasIndex("PermissionId"); 71 | 72 | b.HasIndex("UserId"); 73 | 74 | b.ToTable("UserPermissions"); 75 | }); 76 | 77 | modelBuilder.Entity("API.EF.UserPermission", b => 78 | { 79 | b.HasOne("API.EF.Permission", "Permission") 80 | .WithMany() 81 | .HasForeignKey("PermissionId") 82 | .OnDelete(DeleteBehavior.Cascade) 83 | .IsRequired(); 84 | 85 | b.HasOne("API.EF.User", "User") 86 | .WithMany("Permissions") 87 | .HasForeignKey("UserId") 88 | .OnDelete(DeleteBehavior.Cascade) 89 | .IsRequired(); 90 | 91 | b.Navigation("Permission"); 92 | 93 | b.Navigation("User"); 94 | }); 95 | 96 | modelBuilder.Entity("API.EF.User", b => 97 | { 98 | b.Navigation("Permissions"); 99 | }); 100 | #pragma warning restore 612, 618 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace API 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 22 | } 23 | } -------------------------------------------------------------------------------- /src/API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "API": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": "true", 7 | "launchBrowser": true, 8 | "launchUrl": "swagger", 9 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/API/Startup.cs: -------------------------------------------------------------------------------- 1 | using API.Authorization; 2 | using API.EF; 3 | using API.Configuration; 4 | using API.Infrastructure; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | 12 | namespace API 13 | { 14 | using System.IdentityModel.Tokens.Jwt; 15 | 16 | public class Startup 17 | { 18 | private readonly IConfiguration _configuration; 19 | private AppSettings _appSettings = null!; 20 | 21 | public Startup(IConfiguration configuration) 22 | { 23 | _configuration = configuration; 24 | JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 25 | } 26 | 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | _appSettings = _configuration.ConfigureAndGet(services, AppSettings.SectionName); 30 | 31 | services.AddControllers(); 32 | services.AddDbContext(options => 33 | options.UseSqlServer(_configuration.GetConnectionString("AuthzConnection"))); 34 | 35 | services.AddHostedService(); 36 | 37 | services.AddScoped(); 38 | 39 | services.AddSwagger(_appSettings); 40 | 41 | services.AddAuthentication(_appSettings); 42 | } 43 | 44 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 45 | { 46 | if (env.IsDevelopment()) 47 | app.UseDeveloperExceptionPage(); 48 | 49 | app.UseSwagger(); 50 | app.UseSwaggerUI(options => 51 | { 52 | options.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"); 53 | options.OAuthClientId(_appSettings.Swagger.ClientId); 54 | options.OAuthAppName(_appSettings.Swagger.ClientId); 55 | options.OAuthUsePkce(); 56 | }); 57 | 58 | app.UseHttpsRedirection(); 59 | app.UseRouting(); 60 | app.UseAuthentication(); 61 | 62 | // order here matters - after UseAuthentication so we have the Identity populated in the HttpContext 63 | app.UseMiddleware(); 64 | app.UseAuthorization(); 65 | 66 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "ConnectionStrings": { 10 | "AuthzConnection": "Server=localhost,1433;Database=AuthzApp;User=sa;Password=2@LaiNw)PDvs^t>L!Ybt]6H^%h3U>M" 11 | }, 12 | 13 | "AppSettings" : { 14 | "IdentityServer": { 15 | "BaseUrl": "http://localhost:5002", 16 | "RequireHttps": false 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AppSettings" : { 10 | "Swagger": { 11 | "ClientId": "swagger-ui", 12 | "Title": "API", 13 | "Scopes": { 14 | "api": "The api scope" 15 | } 16 | }, 17 | "IdentityServer": { 18 | "BaseUrl": "", 19 | "Audience": "api", 20 | "RequireHttps": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AuthUtils/AppClaimTypes.cs: -------------------------------------------------------------------------------- 1 | namespace AuthUtils 2 | { 3 | public static class AppClaimTypes 4 | { 5 | /// 6 | /// The custom claim type for the user permissions 7 | /// 8 | public const string Permissions = "permissions"; 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /src/AuthUtils/AuthUtils.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/AuthUtils/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using static Microsoft.AspNetCore.Http.StatusCodes; 8 | 9 | namespace AuthUtils 10 | { 11 | public static class HttpContextExtensions 12 | { 13 | private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions 14 | { 15 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 16 | PropertyNameCaseInsensitive = true 17 | }; 18 | 19 | public static async ValueTask WriteAccessDeniedResponse( 20 | this HttpContext context, 21 | string? title = null, 22 | int? statusCode = null, 23 | CancellationToken cancellationToken = default) 24 | { 25 | var problem = new ProblemDetails 26 | { 27 | Instance = context.Request.Path, 28 | Title = title ?? "Access denied", 29 | Status = statusCode ?? Status403Forbidden 30 | }; 31 | context.Response.StatusCode = problem.Status.Value; 32 | 33 | await context.Response.WriteAsync(JsonSerializer.Serialize(problem, JsonSerializerOptions), 34 | cancellationToken); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/AuthUtils/Permissions.cs: -------------------------------------------------------------------------------- 1 | namespace AuthUtils 2 | { 3 | public static class Permissions 4 | { 5 | public const string Read = "Read"; 6 | public const string Create = "Create"; 7 | public const string Update = "Update"; 8 | public const string Delete = "Delete"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/AuthUtils/PolicyProvider/PermissionAuthorizationPolicyProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.Extensions.Options; 5 | 6 | using static AuthUtils.PolicyProvider.PermissionAuthorizeAttribute; 7 | 8 | namespace AuthUtils.PolicyProvider 9 | { 10 | public class PermissionAuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider 11 | { 12 | public PermissionAuthorizationPolicyProvider(IOptions options) 13 | : base(options) { } 14 | 15 | /// 16 | public override async Task GetPolicyAsync(string policyName) 17 | { 18 | if (!policyName.StartsWith(PolicyPrefix, StringComparison.OrdinalIgnoreCase)) 19 | { 20 | // it's not one of our dynamic policies, so we fallback to the base behavior 21 | // this will load policies added in Startup.cs (AddPolicy..) 22 | return await base.GetPolicyAsync(policyName); 23 | } 24 | 25 | PermissionOperator @operator = GetOperatorFromPolicy(policyName); 26 | string[] permissions = GetPermissionsFromPolicy(policyName); 27 | 28 | // extract the info from the policy name and create our requirement 29 | var requirement = new PermissionRequirement(@operator, permissions); 30 | 31 | // create and return the policy for our requirement 32 | return new AuthorizationPolicyBuilder().AddRequirements(requirement).Build(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AuthUtils/PolicyProvider/PermissionAuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace AuthUtils.PolicyProvider 5 | { 6 | public enum PermissionOperator 7 | { 8 | And = 1, 9 | Or = 2 10 | } 11 | 12 | public class PermissionAuthorizeAttribute : AuthorizeAttribute 13 | { 14 | internal const string PolicyPrefix = "PERMISSION_"; 15 | private const string Separator = "_"; 16 | 17 | /// 18 | /// Initializes the attribute with multiple permissions 19 | /// 20 | /// The operator to use when verifying the permissions provided 21 | /// The list of permissions 22 | public PermissionAuthorizeAttribute(PermissionOperator permissionOperator, params string[] permissions) 23 | { 24 | // E.g: PERMISSION_1_Create_Update.. 25 | Policy = $"{PolicyPrefix}{(int)permissionOperator}{Separator}{string.Join(Separator, permissions)}"; 26 | } 27 | 28 | /// 29 | /// Initializes the attribute with a single permission 30 | /// 31 | /// The permission 32 | public PermissionAuthorizeAttribute(string permission) 33 | { 34 | // E.g: PERMISSION_1_Create.. 35 | Policy = $"{PolicyPrefix}{(int)PermissionOperator.And}{Separator}{permission}"; 36 | } 37 | 38 | public static PermissionOperator GetOperatorFromPolicy(string policyName) 39 | { 40 | var @operator = int.Parse(policyName.AsSpan(PolicyPrefix.Length, 1)); 41 | return (PermissionOperator)@operator; 42 | } 43 | 44 | public static string[] GetPermissionsFromPolicy(string policyName) 45 | { 46 | return policyName.Substring(PolicyPrefix.Length + 2) 47 | .Split(new[] {Separator}, StringSplitOptions.RemoveEmptyEntries); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/AuthUtils/PolicyProvider/PermissionHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace AuthUtils.PolicyProvider 5 | { 6 | public class PermissionHandler : AuthorizationHandler 7 | { 8 | protected override Task HandleRequirementAsync( 9 | AuthorizationHandlerContext context, PermissionRequirement requirement) 10 | { 11 | if (requirement.PermissionOperator == PermissionOperator.And) 12 | { 13 | foreach (var permission in requirement.Permissions) 14 | { 15 | if (!context.User.HasClaim(PermissionRequirement.ClaimType, permission)) 16 | { 17 | context.Fail(); 18 | return Task.CompletedTask; 19 | } 20 | } 21 | 22 | // identity has all required permissions 23 | context.Succeed(requirement); 24 | return Task.CompletedTask; 25 | } 26 | 27 | foreach (var permission in requirement.Permissions) 28 | { 29 | if (context.User.HasClaim(PermissionRequirement.ClaimType, permission)) 30 | { 31 | context.Succeed(requirement); 32 | return Task.CompletedTask; 33 | } 34 | } 35 | 36 | // identity does not have any of the required permissions 37 | context.Fail(); 38 | return Task.CompletedTask; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AuthUtils/PolicyProvider/PermissionRequirement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace AuthUtils.PolicyProvider 5 | { 6 | public class PermissionRequirement : IAuthorizationRequirement 7 | { 8 | public static string ClaimType => AppClaimTypes.Permissions; 9 | 10 | public PermissionOperator PermissionOperator { get; } 11 | 12 | public string[] Permissions { get; } 13 | 14 | public PermissionRequirement(PermissionOperator permissionOperator, string[] permissions) 15 | { 16 | if (permissions.Length == 0) 17 | throw new ArgumentException("At least one permission is required.", nameof(permissions)); 18 | 19 | PermissionOperator = permissionOperator; 20 | Permissions = permissions; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AuthUtils/StandardJwtClaimTypes.cs: -------------------------------------------------------------------------------- 1 | namespace AuthUtils 2 | { 3 | /// 4 | /// List of commonly used claim types 5 | /// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims 6 | /// 7 | public static class StandardJwtClaimTypes 8 | { 9 | public const string Subject = "sub"; 10 | 11 | public const string Name = "name"; 12 | 13 | public const string GivenName = "given_name"; 14 | 15 | public const string FamilyName = "family_name"; 16 | 17 | public const string MiddleName = "middle_name"; 18 | 19 | public const string Email = "email"; 20 | 21 | public const string EmailVerified = "email_verified"; 22 | 23 | public const string PhoneNumber = "phone_number"; 24 | 25 | public const string Address = "address"; 26 | 27 | public const string Audience = "aud"; 28 | 29 | public const string Issuer = "iss"; 30 | 31 | public const string NotBefore = "nbf"; 32 | 33 | public const string Expiration = "exp"; 34 | 35 | public const string UpdatedAt = "updated_at"; 36 | 37 | public const string IssuedAt = "iat"; 38 | 39 | public const string AuthenticationMethod = "amr"; 40 | 41 | public const string SessionId = "sid"; 42 | 43 | public const string ClientId = "client_id"; 44 | 45 | public const string Scope = "scope"; 46 | 47 | public const string IdentityProvider = "idp"; 48 | 49 | public const string Role = "role"; 50 | } 51 | } -------------------------------------------------------------------------------- /src/IdentityServer/.dockerignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | -------------------------------------------------------------------------------- /src/IdentityServer/Config.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | using System.Collections.Generic; 7 | 8 | namespace IdentityServer 9 | { 10 | public static class Config 11 | { 12 | public static IEnumerable IdentityResources => 13 | new IdentityResource[] {new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email()}; 14 | 15 | public static IEnumerable ApiScopes => 16 | new ApiScope[] 17 | { 18 | new ApiScope("api", "API", new [] {"profile", "name", "email", "role", "DateOfBirth"}), 19 | }; 20 | 21 | public static IEnumerable GetApiResource() 22 | { 23 | return new List 24 | { 25 | new ApiResource("api", "API") 26 | { 27 | Scopes = { "api"} 28 | }, 29 | }; 30 | } 31 | 32 | public static IEnumerable Clients => 33 | new Client[] 34 | { 35 | // SwaggerUI client 36 | new Client 37 | { 38 | ClientId = "swagger-ui", 39 | ClientName = "Swagger UI", 40 | ClientSecrets = {new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256())}, 41 | AllowedGrantTypes = GrantTypes.Code, 42 | RequirePkce = true, 43 | RequireClientSecret = false, 44 | RedirectUris = {"https://localhost:5001/swagger/oauth2-redirect.html"}, 45 | AllowedCorsOrigins = {"https://localhost:5001"}, 46 | AllowedScopes = {"openid", "profile", "api", "email"}, 47 | }, 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/IdentityServer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 2 | WORKDIR /source 3 | 4 | # Copy csproj and restore as distinct layers 5 | COPY IdentityServer.csproj ./ 6 | RUN dotnet restore 7 | 8 | # Copy everything else and build 9 | COPY . ./ 10 | RUN dotnet publish -c Release -o /app --no-restore 11 | 12 | # final stage/image 13 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 14 | WORKDIR /app 15 | COPY --from=build /app ./ 16 | ENTRYPOINT ["dotnet", "IdentityServer.dll"] 17 | -------------------------------------------------------------------------------- /src/IdentityServer/IdentityServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | disable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/IdentityServer/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Hosting; 7 | using Serilog; 8 | using Serilog.Events; 9 | using Serilog.Sinks.SystemConsole.Themes; 10 | using System; 11 | 12 | namespace IdentityServer 13 | { 14 | public class Program 15 | { 16 | public static int Main(string[] args) 17 | { 18 | Log.Logger = new LoggerConfiguration() 19 | .MinimumLevel.Debug() 20 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 21 | .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) 22 | .MinimumLevel.Override("System", LogEventLevel.Warning) 23 | .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) 24 | .Enrich.FromLogContext() 25 | // uncomment to write to Azure diagnostics stream 26 | //.WriteTo.File( 27 | // @"D:\home\LogFiles\Application\identityserver.txt", 28 | // fileSizeLimitBytes: 1_000_000, 29 | // rollOnFileSizeLimit: true, 30 | // shared: true, 31 | // flushToDiskInterval: TimeSpan.FromSeconds(1)) 32 | .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code) 33 | .CreateLogger(); 34 | 35 | try 36 | { 37 | Log.Information("Starting host..."); 38 | CreateHostBuilder(args).Build().Run(); 39 | return 0; 40 | } 41 | catch (Exception ex) 42 | { 43 | Log.Fatal(ex, "Host terminated unexpectedly."); 44 | return 1; 45 | } 46 | finally 47 | { 48 | Log.CloseAndFlush(); 49 | } 50 | } 51 | 52 | public static IHostBuilder CreateHostBuilder(string[] args) => 53 | Host.CreateDefaultBuilder(args) 54 | .UseSerilog() 55 | .ConfigureWebHostDefaults(webBuilder => 56 | { 57 | webBuilder.UseStartup(); 58 | }); 59 | } 60 | } -------------------------------------------------------------------------------- /src/IdentityServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SelfHost": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://localhost:5002" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/AccountOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class AccountOptions 10 | { 11 | public static bool AllowLocalLogin = true; 12 | public static bool AllowRememberLogin = true; 13 | public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); 14 | 15 | public static bool ShowLogoutPrompt = true; 16 | public static bool AutomaticRedirectAfterSignOut = false; 17 | 18 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/ExternalProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class ExternalProvider 8 | { 9 | public string DisplayName { get; set; } 10 | public string AuthenticationScheme { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class LoggedOutViewModel 8 | { 9 | public string PostLogoutRedirectUri { get; set; } 10 | public string ClientName { get; set; } 11 | public string SignOutIframeUrl { get; set; } 12 | 13 | public bool AutomaticRedirectAfterSignOut { get; set; } 14 | 15 | public string LogoutId { get; set; } 16 | public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; 17 | public string ExternalAuthenticationScheme { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/LoginInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class LoginInputModel 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | [Required] 14 | public string Password { get; set; } 15 | public bool RememberLogin { get; set; } 16 | public string ReturnUrl { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace IdentityServerHost.Quickstart.UI 10 | { 11 | public class LoginViewModel : LoginInputModel 12 | { 13 | public bool AllowRememberLogin { get; set; } = true; 14 | public bool EnableLocalLogin { get; set; } = true; 15 | 16 | public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); 17 | public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); 18 | 19 | public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; 20 | public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; 21 | } 22 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/LogoutInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class LogoutInputModel 8 | { 9 | public string LogoutId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/LogoutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class LogoutViewModel : LogoutInputModel 8 | { 9 | public bool ShowLogoutPrompt { get; set; } = true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Account/RedirectViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | 6 | namespace IdentityServerHost.Quickstart.UI 7 | { 8 | public class RedirectViewModel 9 | { 10 | public string RedirectUrl { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Consent/ConsentInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ConsentInputModel 10 | { 11 | public string Button { get; set; } 12 | public IEnumerable ScopesConsented { get; set; } 13 | public bool RememberConsent { get; set; } 14 | public string ReturnUrl { get; set; } 15 | public string Description { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 14 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Consent/ConsentViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ConsentViewModel : ConsentInputModel 10 | { 11 | public string ClientName { get; set; } 12 | public string ClientUrl { get; set; } 13 | public string ClientLogoUrl { get; set; } 14 | public bool AllowRememberConsent { get; set; } 15 | 16 | public IEnumerable IdentityScopes { get; set; } 17 | public IEnumerable ApiScopes { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Consent/ProcessConsentResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ProcessConsentResult 10 | { 11 | public bool IsRedirect => RedirectUri != null; 12 | public string RedirectUri { get; set; } 13 | public Client Client { get; set; } 14 | 15 | public bool ShowView => ViewModel != null; 16 | public ConsentViewModel ViewModel { get; set; } 17 | 18 | public bool HasValidationError => ValidationError != null; 19 | public string ValidationError { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Consent/ScopeViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class ScopeViewModel 8 | { 9 | public string Value { get; set; } 10 | public string DisplayName { get; set; } 11 | public string Description { get; set; } 12 | public bool Emphasize { get; set; } 13 | public bool Required { get; set; } 14 | public bool Checked { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Device/DeviceAuthorizationInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class DeviceAuthorizationInputModel : ConsentInputModel 8 | { 9 | public string UserCode { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Device/DeviceAuthorizationViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public class DeviceAuthorizationViewModel : ConsentViewModel 8 | { 9 | public string UserCode { get; set; } 10 | public bool ConfirmUserCode { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Diagnostics/DiagnosticsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace IdentityServerHost.Quickstart.UI 12 | { 13 | [SecurityHeaders] 14 | [Authorize] 15 | public class DiagnosticsController : Controller 16 | { 17 | public async Task Index() 18 | { 19 | var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() }; 20 | if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString())) 21 | { 22 | return NotFound(); 23 | } 24 | 25 | var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync()); 26 | return View(model); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Diagnostics/DiagnosticsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Newtonsoft.Json; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | 11 | namespace IdentityServerHost.Quickstart.UI 12 | { 13 | public class DiagnosticsViewModel 14 | { 15 | public DiagnosticsViewModel(AuthenticateResult result) 16 | { 17 | AuthenticateResult = result; 18 | 19 | if (result.Properties.Items.ContainsKey("client_list")) 20 | { 21 | var encoded = result.Properties.Items["client_list"]; 22 | var bytes = Base64Url.Decode(encoded); 23 | var value = Encoding.UTF8.GetString(bytes); 24 | 25 | Clients = JsonConvert.DeserializeObject(value); 26 | } 27 | } 28 | 29 | public AuthenticateResult AuthenticateResult { get; } 30 | public IEnumerable Clients { get; } = new List(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using IdentityServer4.Models; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace IdentityServerHost.Quickstart.UI 6 | { 7 | public static class Extensions 8 | { 9 | /// 10 | /// Checks if the redirect URI is for a native client. 11 | /// 12 | /// 13 | public static bool IsNativeClient(this AuthorizationRequest context) 14 | { 15 | return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) 16 | && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); 17 | } 18 | 19 | public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri) 20 | { 21 | controller.HttpContext.Response.StatusCode = 200; 22 | controller.HttpContext.Response.Headers["Location"] = ""; 23 | 24 | return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri }); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Grants/GrantsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using IdentityServer4.Stores; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.Authorization; 12 | using IdentityServer4.Events; 13 | using IdentityServer4.Extensions; 14 | 15 | namespace IdentityServerHost.Quickstart.UI 16 | { 17 | /// 18 | /// This sample controller allows a user to revoke grants given to clients 19 | /// 20 | [SecurityHeaders] 21 | [Authorize] 22 | public class GrantsController : Controller 23 | { 24 | private readonly IIdentityServerInteractionService _interaction; 25 | private readonly IClientStore _clients; 26 | private readonly IResourceStore _resources; 27 | private readonly IEventService _events; 28 | 29 | public GrantsController(IIdentityServerInteractionService interaction, 30 | IClientStore clients, 31 | IResourceStore resources, 32 | IEventService events) 33 | { 34 | _interaction = interaction; 35 | _clients = clients; 36 | _resources = resources; 37 | _events = events; 38 | } 39 | 40 | /// 41 | /// Show list of grants 42 | /// 43 | [HttpGet] 44 | public async Task Index() 45 | { 46 | return View("Index", await BuildViewModelAsync()); 47 | } 48 | 49 | /// 50 | /// Handle postback to revoke a client 51 | /// 52 | [HttpPost] 53 | [ValidateAntiForgeryToken] 54 | public async Task Revoke(string clientId) 55 | { 56 | await _interaction.RevokeUserConsentAsync(clientId); 57 | await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId)); 58 | 59 | return RedirectToAction("Index"); 60 | } 61 | 62 | private async Task BuildViewModelAsync() 63 | { 64 | var grants = await _interaction.GetAllUserGrantsAsync(); 65 | 66 | var list = new List(); 67 | foreach(var grant in grants) 68 | { 69 | var client = await _clients.FindClientByIdAsync(grant.ClientId); 70 | if (client != null) 71 | { 72 | var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); 73 | 74 | var item = new GrantViewModel() 75 | { 76 | ClientId = client.ClientId, 77 | ClientName = client.ClientName ?? client.ClientId, 78 | ClientLogoUrl = client.LogoUri, 79 | ClientUrl = client.ClientUri, 80 | Description = grant.Description, 81 | Created = grant.CreationTime, 82 | Expires = grant.Expiration, 83 | IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), 84 | ApiGrantNames = resources.ApiScopes.Select(x => x.DisplayName ?? x.Name).ToArray() 85 | }; 86 | 87 | list.Add(item); 88 | } 89 | } 90 | 91 | return new GrantsViewModel 92 | { 93 | Grants = list 94 | }; 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Grants/GrantsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace IdentityServerHost.Quickstart.UI 9 | { 10 | public class GrantsViewModel 11 | { 12 | public IEnumerable Grants { get; set; } 13 | } 14 | 15 | public class GrantViewModel 16 | { 17 | public string ClientId { get; set; } 18 | public string ClientName { get; set; } 19 | public string ClientUrl { get; set; } 20 | public string ClientLogoUrl { get; set; } 21 | public string Description { get; set; } 22 | public DateTime Created { get; set; } 23 | public DateTime? Expires { get; set; } 24 | public IEnumerable IdentityGrantNames { get; set; } 25 | public IEnumerable ApiGrantNames { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Home/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServerHost.Quickstart.UI 8 | { 9 | public class ErrorViewModel 10 | { 11 | public ErrorViewModel() 12 | { 13 | } 14 | 15 | public ErrorViewModel(string error) 16 | { 17 | Error = new ErrorMessage { Error = error }; 18 | } 19 | 20 | public ErrorMessage Error { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using System.Threading.Tasks; 12 | 13 | namespace IdentityServerHost.Quickstart.UI 14 | { 15 | [SecurityHeaders] 16 | [AllowAnonymous] 17 | public class HomeController : Controller 18 | { 19 | private readonly IIdentityServerInteractionService _interaction; 20 | private readonly IWebHostEnvironment _environment; 21 | private readonly ILogger _logger; 22 | 23 | public HomeController(IIdentityServerInteractionService interaction, IWebHostEnvironment environment, ILogger logger) 24 | { 25 | _interaction = interaction; 26 | _environment = environment; 27 | _logger = logger; 28 | } 29 | 30 | public IActionResult Index() 31 | { 32 | if (_environment.IsDevelopment()) 33 | { 34 | // only show in development 35 | return View(); 36 | } 37 | 38 | _logger.LogInformation("Homepage is disabled in production. Returning 404."); 39 | return NotFound(); 40 | } 41 | 42 | /// 43 | /// Shows the error page 44 | /// 45 | public async Task Error(string errorId) 46 | { 47 | var vm = new ErrorViewModel(); 48 | 49 | // retrieve error details from identityserver 50 | var message = await _interaction.GetErrorContextAsync(errorId); 51 | if (message != null) 52 | { 53 | vm.Error = message; 54 | 55 | if (!_environment.IsDevelopment()) 56 | { 57 | // only show in development 58 | message.ErrorDescription = null; 59 | } 60 | } 61 | 62 | return View("Error", vm); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/SecurityHeadersAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | 8 | namespace IdentityServerHost.Quickstart.UI 9 | { 10 | public class SecurityHeadersAttribute : ActionFilterAttribute 11 | { 12 | public override void OnResultExecuting(ResultExecutingContext context) 13 | { 14 | var result = context.Result; 15 | if (result is ViewResult) 16 | { 17 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 18 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) 19 | { 20 | context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); 21 | } 22 | 23 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 24 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) 25 | { 26 | context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); 27 | } 28 | 29 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 30 | var csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; 31 | // also consider adding upgrade-insecure-requests once you have HTTPS in place for production 32 | //csp += "upgrade-insecure-requests;"; 33 | // also an example if you need client images to be displayed from twitter 34 | // csp += "img-src 'self' https://pbs.twimg.com;"; 35 | 36 | // once for standards compliant browsers 37 | if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) 38 | { 39 | context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); 40 | } 41 | // and once again for IE 42 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) 43 | { 44 | context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); 45 | } 46 | 47 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 48 | var referrer_policy = "no-referrer"; 49 | if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) 50 | { 51 | context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/IdentityServer/Quickstart/TestUsers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using IdentityServer4.Test; 7 | using System.Collections.Generic; 8 | using System.Security.Claims; 9 | using System.Text.Json; 10 | using IdentityServer4; 11 | 12 | namespace IdentityServerHost.Quickstart.UI 13 | { 14 | public class TestUsers 15 | { 16 | public static List Users 17 | { 18 | get 19 | { 20 | var address = new 21 | { 22 | street_address = "One Hacker Way", 23 | locality = "Heidelberg", 24 | postal_code = 69118, 25 | country = "Germany" 26 | }; 27 | 28 | return new List 29 | { 30 | new TestUser 31 | { 32 | SubjectId = "818727", 33 | Username = "alice", 34 | Password = "alice", 35 | Claims = 36 | { 37 | new Claim(JwtClaimTypes.Name, "Alice Smith"), 38 | new Claim(JwtClaimTypes.GivenName, "Alice"), 39 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 40 | new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), 41 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 42 | new Claim(JwtClaimTypes.WebSite, "http://alice.com"), 43 | new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json), 44 | new Claim(JwtClaimTypes.Role, "Manager"), 45 | new Claim("DateOfBirth", "2006-01-01") 46 | } 47 | }, 48 | new TestUser 49 | { 50 | SubjectId = "88421113", 51 | Username = "bob", 52 | Password = "bob", 53 | Claims = 54 | { 55 | new Claim(JwtClaimTypes.Name, "Bob Smith"), 56 | new Claim(JwtClaimTypes.GivenName, "Bob"), 57 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 58 | new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), 59 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 60 | new Claim(JwtClaimTypes.WebSite, "http://bob.com"), 61 | new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json), 62 | new Claim(JwtClaimTypes.Role, "Staff"), 63 | new Claim("DateOfBirth", "1988-01-01") 64 | } 65 | } 66 | }; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/IdentityServer/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4; 6 | using IdentityServerHost.Quickstart.UI; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | 13 | namespace IdentityServer 14 | { 15 | public class Startup 16 | { 17 | public IWebHostEnvironment Environment { get; } 18 | public IConfiguration Configuration { get; } 19 | 20 | public Startup(IWebHostEnvironment environment, IConfiguration configuration) 21 | { 22 | Environment = environment; 23 | Configuration = configuration; 24 | } 25 | 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddControllersWithViews(); 29 | 30 | var builder = services.AddIdentityServer(options => 31 | { 32 | options.Events.RaiseErrorEvents = true; 33 | options.Events.RaiseInformationEvents = true; 34 | options.Events.RaiseFailureEvents = true; 35 | options.Events.RaiseSuccessEvents = true; 36 | 37 | // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html 38 | options.EmitStaticAudienceClaim = true; 39 | }) 40 | .AddTestUsers(TestUsers.Users); 41 | 42 | // in-memory, code config 43 | builder.AddInMemoryIdentityResources(Config.IdentityResources); 44 | builder.AddInMemoryApiScopes(Config.ApiScopes); 45 | builder.AddInMemoryApiResources(Config.GetApiResource()); 46 | builder.AddInMemoryClients(Config.Clients); 47 | 48 | // not recommended for production - you need to store your key material somewhere secure 49 | builder.AddDeveloperSigningCredential(); 50 | 51 | services.AddAuthentication(); 52 | } 53 | 54 | public void Configure(IApplicationBuilder app) 55 | { 56 | if (Environment.IsDevelopment()) 57 | { 58 | app.UseDeveloperExceptionPage(); 59 | } 60 | 61 | app.UseStaticFiles(); 62 | 63 | app.UseRouting(); 64 | app.UseIdentityServer(); 65 | app.UseAuthorization(); 66 | app.UseEndpoints(endpoints => 67 | { 68 | endpoints.MapDefaultControllerRoute(); 69 | }); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/IdentityServer/Views/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Access Denied

5 |

You do not have access to that resource.

6 |
7 |
-------------------------------------------------------------------------------- /src/IdentityServer/Views/Account/LoggedOut.cshtml: -------------------------------------------------------------------------------- 1 | @model LoggedOutViewModel 2 | 3 | @{ 4 | // set this so the layout rendering sees an anonymous user 5 | ViewData["signed-out"] = true; 6 | } 7 | 8 |
9 |

10 | Logout 11 | You are now logged out 12 |

13 | 14 | @if (Model.PostLogoutRedirectUri != null) 15 | { 16 |
17 | Click here to return to the 18 | @Model.ClientName application. 19 |
20 | } 21 | 22 | @if (Model.SignOutIframeUrl != null) 23 | { 24 | 25 | } 26 |
27 | 28 | @section scripts 29 | { 30 | @if (Model.AutomaticRedirectAfterSignOut) 31 | { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model LoginViewModel 2 | 3 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @model LogoutViewModel 2 | 3 |
4 |
5 |

Logout

6 |

Would you like to logut of IdentityServer?

7 |
8 | 9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Device/Success.cshtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |

Success

5 |

You have successfully authorized the device

6 |
7 |
8 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Device/UserCodeCapture.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 |
4 |
5 |

User Code

6 |

Please enter the code displayed on your device.

7 |
8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Diagnostics/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model DiagnosticsViewModel 2 | 3 |
4 |
5 |

Authentication Cookie

6 |
7 | 8 |
9 |
10 |
11 |
12 |

Claims

13 |
14 |
15 |
16 | @foreach (var claim in Model.AuthenticateResult.Principal.Claims) 17 | { 18 |
@claim.Type
19 |
@claim.Value
20 | } 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 |

Properties

30 |
31 |
32 |
33 | @foreach (var prop in Model.AuthenticateResult.Properties.Items) 34 | { 35 |
@prop.Key
36 |
@prop.Value
37 | } 38 | @if (Model.Clients.Any()) 39 | { 40 |
Clients
41 |
42 | @{ 43 | var clients = Model.Clients.ToArray(); 44 | for(var i = 0; i < clients.Length; i++) 45 | { 46 | @clients[i] 47 | if (i < clients.Length - 1) 48 | { 49 | , 50 | } 51 | } 52 | } 53 |
54 | } 55 |
56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Grants/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model GrantsViewModel 2 | 3 |
4 |
5 |

Client Application Permissions

6 |

Below is the list of applications you have given permission to and the resources they have access to.

7 |
8 | 9 | @if (Model.Grants.Any() == false) 10 | { 11 |
12 |
13 |
14 | You have not given access to any applications 15 |
16 |
17 |
18 | } 19 | else 20 | { 21 | foreach (var grant in Model.Grants) 22 | { 23 |
24 |
25 |
26 |
27 | @if (grant.ClientLogoUrl != null) 28 | { 29 | 30 | } 31 | @grant.ClientName 32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 | 43 |
    44 | @if (grant.Description != null) 45 | { 46 |
  • 47 | @grant.Description 48 |
  • 49 | } 50 |
  • 51 | @grant.Created.ToString("yyyy-MM-dd") 52 |
  • 53 | @if (grant.Expires.HasValue) 54 | { 55 |
  • 56 | @grant.Expires.Value.ToString("yyyy-MM-dd") 57 |
  • 58 | } 59 | @if (grant.IdentityGrantNames.Any()) 60 | { 61 |
  • 62 | 63 |
      64 | @foreach (var name in grant.IdentityGrantNames) 65 | { 66 |
    • @name
    • 67 | } 68 |
    69 |
  • 70 | } 71 | @if (grant.ApiGrantNames.Any()) 72 | { 73 |
  • 74 | 75 |
      76 | @foreach (var name in grant.ApiGrantNames) 77 | { 78 |
    • @name
    • 79 | } 80 |
    81 |
  • 82 | } 83 |
84 |
85 | } 86 | } 87 |
-------------------------------------------------------------------------------- /src/IdentityServer/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Diagnostics 2 | 3 | @{ 4 | var version = FileVersionInfo.GetVersionInfo(typeof(IdentityServer4.Hosting.IdentityServerMiddleware).Assembly.Location).ProductVersion.Split('+').First(); 5 | } 6 | 7 |
8 |

9 | 10 | Welcome to IdentityServer4 11 | (version @version) 12 |

13 | 14 |
    15 |
  • 16 | IdentityServer publishes a 17 | discovery document 18 | where you can find metadata and links to all the endpoints, key material, etc. 19 |
  • 20 |
  • 21 | Click here to see the claims for your current session. 22 |
  • 23 |
  • 24 | Click here to manage your stored grants. 25 |
  • 26 |
  • 27 | Here are links to the 28 | source code repository, 29 | and ready to use samples. 30 |
  • 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | 3 | @{ 4 | var error = Model?.Error?.Error; 5 | var errorDescription = Model?.Error?.ErrorDescription; 6 | var request_id = Model?.Error?.RequestId; 7 | } 8 | 9 |
10 |
11 |

Error

12 |
13 | 14 |
15 |
16 |
17 | Sorry, there was an error 18 | 19 | @if (error != null) 20 | { 21 | 22 | 23 | : @error 24 | 25 | 26 | 27 | if (errorDescription != null) 28 | { 29 |
@errorDescription
30 | } 31 | } 32 |
33 | 34 | @if (request_id != null) 35 | { 36 |
Request Id: @request_id
37 | } 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/Redirect.cshtml: -------------------------------------------------------------------------------- 1 | @model RedirectViewModel 2 | 3 |
4 |
5 |

You are now being returned to the application

6 |

Once complete, you may close this tab.

7 |
8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | IdentityServer4 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | @RenderBody() 21 |
22 | 23 | 24 | 25 | 26 | @RenderSection("scripts", required: false) 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/_Nav.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Extensions 2 | 3 | @{ 4 | string name = null; 5 | if (!true.Equals(ViewData["signed-out"])) 6 | { 7 | name = Context.User?.GetDisplayName(); 8 | } 9 | } 10 | 11 | 34 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @model ScopeViewModel 2 | 3 |
  • 4 | 24 | @if (Model.Required) 25 | { 26 | (required) 27 | } 28 | @if (Model.Description != null) 29 | { 30 | 33 | } 34 |
  • -------------------------------------------------------------------------------- /src/IdentityServer/Views/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /src/IdentityServer/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServerHost.Quickstart.UI 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /src/IdentityServer/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/IdentityServer/tempkey.jwk: -------------------------------------------------------------------------------- 1 | {"alg":"RS256","d":"UPglWQhF6KHKq21BFvEXHjmdqG8sOPi9zuf4bFWOAbuoAvKtwIH2wFUWXaVL1lBE4PaAy7tVmU5M7zBHKdU3XKblADnQZJDXlk7lqi-MDFFy2aHgC3IeWo7ktv1mIxrwtjdAjPuas8BWg-7FSpzzCGiJqtXvj5fGcrz1nMe09demYpaiRPH0236xGvq5RXYIj2lnJg2c5YHSglFMagRr8h_It-AIgTe1uawZ9ZWtKAUDVbtYW2CA2ylSf65gD-pPuBFi5-vDd3QtWt16le0EKQWx7VfoG1zg9WYebs2GsE5D9-p319nHz_a-V3XmFivbuhjtFvB2EumoSEVOQXZYYQ","dp":"TBzZ098P3LhGjvM2_y5A5ueSgha6j4S2CYkvvSmKO_tTzvfcFEXaEyvhbx2isVLPUNEB1JnT3w4H0az_eUh_xtYTTc12UqaraDrG2kwb7SG-QfdTorGM92BLhs1Bzqg4DaZnE1QkZ4DEHSiIchmUs7hbxEdSnMzomOZpVp92tgk","dq":"a0cGtforjBZNb-Uid3O1-XAWvxC38XhrXqzee5iDywUo58q1B8VFTSxukU0iz1eb-Dpkw-qA4VwyI1G706nG2IGp517gJ6N3GNOcd6ZYEeti2xU9iT2LIqSKF-OPwa5LBNzslf5twXbgEGWdLrGR5FdF9vvSj-gNji8yCshAVSU","e":"AQAB","kid":"4235614A2ACE39057CC2D40F16A32285","kty":"RSA","n":"qiwjLfqW4iwMB2NVC3yY_Q8MF9j15SmavWj8sawI9YN5g-jJ-3t9rud70KTtOLAr4dlps1b93NAVIJgHPeUyfwVkD0KX2RBUhOJ0E4QWEPkrxjXaMoomR1oHWtcVmoWRlyFYcaets6BcPswuzlvHyrMiVeseNzZbKeOD-865Ta6zUPpD9w4r1RZL5NAiHQZTR3RzCI-jCLYb_p_b6YioSzmHSb-i1HDDDFeoyv0Y-z2mUNCOLZRjLxl6_HmVGwViHG-aeFw8ki0k2NScNr2F5_Cu8lMWU-nKLCHxYl28dFVOwBHXLIds1bXTl7xISvlCQnnwwzF2xp6vxrD5QxM7XQ","p":"2WXlsxfAIl2ONgrjhTJJOdUmbrb2LF_38oN3pEIWiDN_uAK2MnlZQdZvcskBIzerg5_EFPi4PHK6KhJJxiOO8t3OFOq_Qauyjq3ichTBNd-_xsJlqfJ_ACi7yVbA3GXfVJwHeOajC85ebteNEQPQQxahzr4JtUOpQzAws4EuSQk","q":"yGOKJzfaC46BeMFJ5YeD0iJeQDhwYhGIUKwtnxvgLv8whZUrcJ_rWp01vLzz4kbfsmk1iYNWMX4_sgrlFB0d9FgDoGuxn8FqWX2AyUn6Uz_1xg0nJkr_MQI3WtjWNQCrSbs7r3BQb8JAxgqBBSZKEvcI5Xm18BkU6AlIaxJq2LU","qi":"pHYT90k9BUeidMlOxmLD2NPnkzt2Szdukx62C8WdWOhYu4c5tYTjsD-FVVOcmx3orzdf0thoiBJ4VvfGtdpevLPYsMUP6gWKs7pidsDjvXEw3HO39fpFsRccP9C0Wx3W5n5A9YtjKsR3KbmcAjmgzgJKJlrqIDjyura1XWx7CIY"} -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | .body-container { 2 | margin-top: 60px; 3 | padding-bottom: 40px; } 4 | 5 | .welcome-page li { 6 | list-style: none; 7 | padding: 4px; } 8 | 9 | .logged-out-page iframe { 10 | display: none; 11 | width: 0; 12 | height: 0; } 13 | 14 | .grants-page .card { 15 | margin-top: 20px; 16 | border-bottom: 1px solid lightgray; } 17 | .grants-page .card .card-title { 18 | font-size: 120%; 19 | font-weight: bold; } 20 | .grants-page .card .card-title img { 21 | width: 100px; 22 | height: 100px; } 23 | .grants-page .card label { 24 | font-weight: bold; } 25 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | .body-container{margin-top:60px;padding-bottom:40px;}.welcome-page li{list-style:none;padding:4px;}.logged-out-page iframe{display:none;width:0;height:0;}.grants-page .card{margin-top:20px;border-bottom:1px solid #d3d3d3;}.grants-page .card .card-title{font-size:120%;font-weight:bold;}.grants-page .card .card-title img{width:100px;height:100px;}.grants-page .card label{font-weight:bold;} -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/css/site.scss: -------------------------------------------------------------------------------- 1 | .body-container { 2 | margin-top: 60px; 3 | padding-bottom:40px; 4 | } 5 | 6 | .welcome-page { 7 | li { 8 | list-style: none; 9 | padding: 4px; 10 | } 11 | } 12 | 13 | .logged-out-page { 14 | iframe { 15 | display: none; 16 | width: 0; 17 | height: 0; 18 | } 19 | } 20 | 21 | .grants-page { 22 | .card { 23 | margin-top: 20px; 24 | border-bottom: 1px solid lightgray; 25 | 26 | .card-title { 27 | img { 28 | width: 100px; 29 | height: 100px; 30 | } 31 | 32 | font-size: 120%; 33 | font-weight: bold; 34 | } 35 | 36 | label { 37 | font-weight: bold; 38 | } 39 | } 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaopgrassi/authz-custom-middleware/2d3cd1ef8898c8aeaad79e04d3d986acb2433449/src/IdentityServer/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaopgrassi/authz-custom-middleware/2d3cd1ef8898c8aeaad79e04d3d986acb2433449/src/IdentityServer/wwwroot/icon.jpg -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joaopgrassi/authz-custom-middleware/2d3cd1ef8898c8aeaad79e04d3d986acb2433449/src/IdentityServer/wwwroot/icon.png -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/js/signin-redirect.js: -------------------------------------------------------------------------------- 1 | window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); 2 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/js/signout-redirect.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | var a = document.querySelector("a.PostLogoutRedirectUri"); 3 | if (a) { 4 | window.location = a.href; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors 4 | * Copyright 2011-2019 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/scss/_alert.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Base styles 3 | // 4 | 5 | .alert { 6 | position: relative; 7 | padding: $alert-padding-y $alert-padding-x; 8 | margin-bottom: $alert-margin-bottom; 9 | border: $alert-border-width solid transparent; 10 | @include border-radius($alert-border-radius); 11 | } 12 | 13 | // Headings for larger alerts 14 | .alert-heading { 15 | // Specified to prevent conflicts of changing $headings-color 16 | color: inherit; 17 | } 18 | 19 | // Provide class for links that match alerts 20 | .alert-link { 21 | font-weight: $alert-link-font-weight; 22 | } 23 | 24 | 25 | // Dismissible alerts 26 | // 27 | // Expand the right padding and account for the close button's positioning. 28 | 29 | .alert-dismissible { 30 | padding-right: $close-font-size + $alert-padding-x * 2; 31 | 32 | // Adjust close link position 33 | .close { 34 | position: absolute; 35 | top: 0; 36 | right: 0; 37 | padding: $alert-padding-y $alert-padding-x; 38 | color: inherit; 39 | } 40 | } 41 | 42 | 43 | // Alternate styles 44 | // 45 | // Generate contextual modifier classes for colorizing the alert. 46 | 47 | @each $color, $value in $theme-colors { 48 | .alert-#{$color} { 49 | @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/scss/_badge.scss: -------------------------------------------------------------------------------- 1 | // Base class 2 | // 3 | // Requires one of the contextual, color modifier classes for `color` and 4 | // `background-color`. 5 | 6 | .badge { 7 | display: inline-block; 8 | padding: $badge-padding-y $badge-padding-x; 9 | @include font-size($badge-font-size); 10 | font-weight: $badge-font-weight; 11 | line-height: 1; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | @include border-radius($badge-border-radius); 16 | @include transition($badge-transition); 17 | 18 | @at-root a#{&} { 19 | @include hover-focus() { 20 | text-decoration: none; 21 | } 22 | } 23 | 24 | // Empty badges collapse automatically 25 | &:empty { 26 | display: none; 27 | } 28 | } 29 | 30 | // Quick fix for badges in buttons 31 | .btn .badge { 32 | position: relative; 33 | top: -1px; 34 | } 35 | 36 | // Pill badges 37 | // 38 | // Make them extra rounded with a modifier to replace v3's badges. 39 | 40 | .badge-pill { 41 | padding-right: $badge-pill-padding-x; 42 | padding-left: $badge-pill-padding-x; 43 | @include border-radius($badge-pill-border-radius); 44 | } 45 | 46 | // Colors 47 | // 48 | // Contextual variations (linked badges get darker on :hover). 49 | 50 | @each $color, $value in $theme-colors { 51 | .badge-#{$color} { 52 | @include badge-variant($value); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/IdentityServer/wwwroot/lib/bootstrap/scss/_breadcrumb.scss: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | display: flex; 3 | flex-wrap: wrap; 4 | padding: $breadcrumb-padding-y $breadcrumb-padding-x; 5 | margin-bottom: $breadcrumb-margin-bottom; 6 | @include font-size($breadcrumb-font-size); 7 | list-style: none; 8 | background-color: $breadcrumb-bg; 9 | @include border-radius($breadcrumb-border-radius); 10 | } 11 | 12 | .breadcrumb-item { 13 | // The separator between breadcrumbs (by default, a forward-slash: "/") 14 | + .breadcrumb-item { 15 | padding-left: $breadcrumb-item-padding; 16 | 17 | &::before { 18 | display: inline-block; // Suppress underlining of the separator in modern browsers 19 | padding-right: $breadcrumb-item-padding; 20 | color: $breadcrumb-divider-color; 21 | content: escape-svg($breadcrumb-divider); 22 | } 23 | } 24 | 25 | // IE9-11 hack to properly handle hyperlink underlines for breadcrumbs built 26 | // without `