├── .gitattributes ├── .gitignore ├── QuickstartIdentityServer ├── Config.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Quickstart │ ├── Account │ │ ├── AccountController.cs │ │ ├── AccountOptions.cs │ │ ├── ExternalProvider.cs │ │ ├── LoggedOutViewModel.cs │ │ ├── LoginInputModel.cs │ │ ├── LoginViewModel.cs │ │ ├── LogoutInputModel.cs │ │ └── LogoutViewModel.cs │ ├── Consent │ │ ├── ConsentController.cs │ │ ├── ConsentInputModel.cs │ │ ├── ConsentOptions.cs │ │ ├── ConsentViewModel.cs │ │ ├── ProcessConsentResult.cs │ │ └── ScopeViewModel.cs │ ├── Diagnostics │ │ ├── DiagnosticsController.cs │ │ └── DiagnosticsViewModel.cs │ ├── Grants │ │ ├── GrantsController.cs │ │ └── GrantsViewModel.cs │ ├── Home │ │ ├── ErrorViewModel.cs │ │ └── HomeController.cs │ ├── SecurityHeadersAttribute.cs │ └── TestUsers.cs ├── QuickstartIdentityServer.csproj ├── Startup.cs ├── Views │ ├── Account │ │ ├── LoggedOut.cshtml │ │ ├── Login.cshtml │ │ └── Logout.cshtml │ ├── Consent │ │ ├── Index.cshtml │ │ └── _ScopeListItem.cshtml │ ├── Diagnostics │ │ └── Index.cshtml │ ├── Grants │ │ └── Index.cshtml │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ └── _ValidationSummary.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── tempkey.rsa └── wwwroot │ ├── css │ ├── site.css │ ├── site.less │ └── site.min.css │ ├── favicon.ico │ ├── icon.jpg │ ├── icon.png │ ├── js │ └── signout-redirect.js │ └── lib │ ├── bootstrap │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js │ └── jquery │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── VueApi.sln ├── VueApi ├── Controllers │ ├── CatchAllController.cs │ ├── ServicesController.cs │ └── ValuesController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── VueApi.csproj ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── favicon.ico │ └── img │ └── logo.82b9c7a5.png └── vue-app ├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html └── static │ ├── oidc-client.min.js │ └── silent-renew.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ └── HelloWorld.vue ├── main.js ├── router.js ├── services │ └── security.js └── views │ ├── About.vue │ ├── Callback.vue │ ├── Home.vue │ └── Unauthorized.vue └── vue.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | yarn.lock 264 | 265 | VueApi/wwwroot/ -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | using IdentityServer4; 5 | using IdentityServer4.Models; 6 | using IdentityServer4.Test; 7 | using System.Collections.Generic; 8 | using System.Security.Claims; 9 | 10 | namespace QuickstartIdentityServer 11 | { 12 | public class Config 13 | { 14 | // scopes define the resources in your system 15 | public static IEnumerable GetIdentityResources() 16 | { 17 | return new List 18 | { 19 | new IdentityResources.OpenId(), 20 | new IdentityResources.Profile(), 21 | }; 22 | } 23 | 24 | public static IEnumerable GetApiResources() 25 | { 26 | return new List 27 | { 28 | new ApiResource("api1", "My API") 29 | }; 30 | } 31 | 32 | // clients want to access resources (aka scopes) 33 | public static IEnumerable GetClients() 34 | { 35 | // client credentials client 36 | return new List 37 | { 38 | new Client 39 | { 40 | ClientId = "client", 41 | AllowedGrantTypes = GrantTypes.ClientCredentials, 42 | 43 | ClientSecrets = 44 | { 45 | new Secret("secret".Sha256()) 46 | }, 47 | AllowedScopes = { "api1" } 48 | }, 49 | 50 | // resource owner password grant client 51 | new Client 52 | { 53 | ClientId = "ro.client", 54 | AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, 55 | 56 | ClientSecrets = 57 | { 58 | new Secret("secret".Sha256()) 59 | }, 60 | AllowedScopes = { "api1" } 61 | }, 62 | 63 | // OpenID Connect hybrid flow and client credentials client (MVC) 64 | new Client 65 | { 66 | ClientId = "mvc", 67 | ClientName = "MVC Client", 68 | AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, 69 | 70 | ClientSecrets = 71 | { 72 | new Secret("secret".Sha256()) 73 | }, 74 | 75 | RedirectUris = { "http://localhost:5002/signin-oidc" }, 76 | PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, 77 | 78 | AllowedScopes = 79 | { 80 | IdentityServerConstants.StandardScopes.OpenId, 81 | IdentityServerConstants.StandardScopes.Profile, 82 | "api1" 83 | }, 84 | AllowOfflineAccess = true 85 | }, 86 | 87 | // JavaScript Client 88 | new Client 89 | { 90 | ClientId = "js", 91 | ClientName = "VueJs JavaScript Client", 92 | AllowedGrantTypes = GrantTypes.Implicit, 93 | AllowAccessTokensViaBrowser = true, 94 | 95 | AllowOfflineAccess = true, 96 | AccessTokenLifetime = 90, // 1.5 minutes 97 | AbsoluteRefreshTokenLifetime = 0, 98 | RefreshTokenUsage = TokenUsage.OneTimeOnly, 99 | RefreshTokenExpiration = TokenExpiration.Sliding, 100 | UpdateAccessTokenClaimsOnRefresh = true, 101 | RequireConsent = false, 102 | 103 | RedirectUris = { 104 | "https://localhost:5000/callback", 105 | "https://localhost:5000/static/silent-renew.html" 106 | }, 107 | 108 | PostLogoutRedirectUris = { "https://localhost:5000/" }, 109 | AllowedCorsOrigins = { "https://localhost:5000" }, 110 | 111 | AllowedScopes = 112 | { 113 | IdentityServerConstants.StandardScopes.OpenId, 114 | IdentityServerConstants.StandardScopes.Profile, 115 | "api1" 116 | }, 117 | } 118 | }; 119 | } 120 | 121 | public static List GetUsers() 122 | { 123 | return new List 124 | { 125 | new TestUser 126 | { 127 | SubjectId = "1", 128 | Username = "alice", 129 | Password = "password", 130 | 131 | Claims = new List 132 | { 133 | new Claim("name", "Alice"), 134 | new Claim("website", "https://alice.com") 135 | } 136 | }, 137 | new TestUser 138 | { 139 | SubjectId = "2", 140 | Username = "bob", 141 | Password = "password", 142 | 143 | Claims = new List 144 | { 145 | new Claim("name", "Bob"), 146 | new Claim("website", "https://bob.com") 147 | } 148 | } 149 | }; 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | using System; 5 | using System.Net; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | 9 | namespace QuickstartIdentityServer 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | Console.Title = "IdentityServer"; 16 | 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5010/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "QuickstartIdentityServer": { 19 | "commandName": "Project", 20 | "launchUrl": "http://localhost:5000", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5443/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/Quickstart/Account/AccountController.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.Services; 7 | using IdentityServer4.Stores; 8 | using IdentityServer4.Test; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Security.Claims; 15 | using System.Security.Principal; 16 | using System.Threading.Tasks; 17 | using Microsoft.AspNetCore.Authentication; 18 | using IdentityServer4.Events; 19 | using IdentityServer4.Extensions; 20 | using IdentityServer4.Models; 21 | 22 | namespace IdentityServer4.Quickstart.UI 23 | { 24 | /// 25 | /// This sample controller implements a typical login/logout/provision workflow for local and external accounts. 26 | /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production! 27 | /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval 28 | /// 29 | [SecurityHeaders] 30 | public class AccountController : Controller 31 | { 32 | private readonly TestUserStore _users; 33 | private readonly IIdentityServerInteractionService _interaction; 34 | private readonly IClientStore _clientStore; 35 | private readonly IAuthenticationSchemeProvider _schemeProvider; 36 | private readonly IEventService _events; 37 | 38 | public AccountController( 39 | IIdentityServerInteractionService interaction, 40 | IClientStore clientStore, 41 | IAuthenticationSchemeProvider schemeProvider, 42 | IEventService events, 43 | TestUserStore users = null) 44 | { 45 | // if the TestUserStore is not in DI, then we'll just use the global users collection 46 | // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) 47 | _users = users ?? new TestUserStore(TestUsers.Users); 48 | 49 | _interaction = interaction; 50 | _clientStore = clientStore; 51 | _schemeProvider = schemeProvider; 52 | _events = events; 53 | } 54 | 55 | /// 56 | /// Show login page 57 | /// 58 | [HttpGet] 59 | public async Task Login(string returnUrl) 60 | { 61 | // build a model so we know what to show on the login page 62 | var vm = await BuildLoginViewModelAsync(returnUrl); 63 | 64 | if (vm.IsExternalLoginOnly) 65 | { 66 | // we only have one option for logging in and it's an external provider 67 | return await ExternalLogin(vm.ExternalLoginScheme, returnUrl); 68 | } 69 | 70 | return View(vm); 71 | } 72 | 73 | /// 74 | /// Handle postback from username/password login 75 | /// 76 | [HttpPost] 77 | [ValidateAntiForgeryToken] 78 | public async Task Login(LoginInputModel model, string button) 79 | { 80 | if (button != "login") 81 | { 82 | // the user clicked the "cancel" button 83 | var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); 84 | if (context != null) 85 | { 86 | // if the user cancels, send a result back into IdentityServer as if they 87 | // denied the consent (even if this client does not require consent). 88 | // this will send back an access denied OIDC error response to the client. 89 | await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); 90 | 91 | // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null 92 | return Redirect(model.ReturnUrl); 93 | } 94 | else 95 | { 96 | // since we don't have a valid context, then we just go back to the home page 97 | return Redirect("~/"); 98 | } 99 | } 100 | 101 | if (ModelState.IsValid) 102 | { 103 | // validate username/password against in-memory store 104 | if (_users.ValidateCredentials(model.Username, model.Password)) 105 | { 106 | var user = _users.FindByUsername(model.Username); 107 | await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); 108 | 109 | // only set explicit expiration here if user chooses "remember me". 110 | // otherwise we rely upon expiration configured in cookie middleware. 111 | AuthenticationProperties props = null; 112 | if (AccountOptions.AllowRememberLogin && model.RememberLogin) 113 | { 114 | props = new AuthenticationProperties 115 | { 116 | IsPersistent = true, 117 | ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) 118 | }; 119 | }; 120 | 121 | // issue authentication cookie with subject ID and username 122 | await HttpContext.SignInAsync(user.SubjectId, user.Username, props); 123 | 124 | // make sure the returnUrl is still valid, and if so redirect back to authorize endpoint or a local page 125 | // the IsLocalUrl check is only necessary if you want to support additional local pages, otherwise IsValidReturnUrl is more strict 126 | if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) 127 | { 128 | return Redirect(model.ReturnUrl); 129 | } 130 | 131 | return Redirect("~/"); 132 | } 133 | 134 | await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); 135 | 136 | ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); 137 | } 138 | 139 | // something went wrong, show form with error 140 | var vm = await BuildLoginViewModelAsync(model); 141 | return View(vm); 142 | } 143 | 144 | /// 145 | /// initiate roundtrip to external authentication provider 146 | /// 147 | [HttpGet] 148 | public async Task ExternalLogin(string provider, string returnUrl) 149 | { 150 | if (AccountOptions.WindowsAuthenticationSchemeName == provider) 151 | { 152 | // windows authentication needs special handling 153 | return await ProcessWindowsLoginAsync(returnUrl); 154 | } 155 | else 156 | { 157 | // start challenge and roundtrip the return URL and 158 | var props = new AuthenticationProperties() 159 | { 160 | RedirectUri = Url.Action("ExternalLoginCallback"), 161 | Items = 162 | { 163 | { "returnUrl", returnUrl }, 164 | { "scheme", provider }, 165 | } 166 | }; 167 | return Challenge(props, provider); 168 | } 169 | } 170 | 171 | /// 172 | /// Post processing of external authentication 173 | /// 174 | [HttpGet] 175 | public async Task ExternalLoginCallback() 176 | { 177 | // read external identity from the temporary cookie 178 | var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); 179 | if (result?.Succeeded != true) 180 | { 181 | throw new Exception("External authentication error"); 182 | } 183 | 184 | // lookup our user and external provider info 185 | var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result); 186 | if (user == null) 187 | { 188 | // this might be where you might initiate a custom workflow for user registration 189 | // in this sample we don't show how that would be done, as our sample implementation 190 | // simply auto-provisions new external user 191 | user = AutoProvisionUser(provider, providerUserId, claims); 192 | } 193 | 194 | // this allows us to collect any additonal claims or properties 195 | // for the specific prtotocols used and store them in the local auth cookie. 196 | // this is typically used to store data needed for signout from those protocols. 197 | var additionalLocalClaims = new List(); 198 | var localSignInProps = new AuthenticationProperties(); 199 | ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps); 200 | ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps); 201 | ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps); 202 | 203 | // issue authentication cookie for user 204 | await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username)); 205 | await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray()); 206 | 207 | // delete temporary cookie used during external authentication 208 | await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); 209 | 210 | // validate return URL and redirect back to authorization endpoint or a local page 211 | var returnUrl = result.Properties.Items["returnUrl"]; 212 | if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl)) 213 | { 214 | return Redirect(returnUrl); 215 | } 216 | 217 | return Redirect("~/"); 218 | } 219 | 220 | /// 221 | /// Show logout page 222 | /// 223 | [HttpGet] 224 | public async Task Logout(string logoutId) 225 | { 226 | // build a model so the logout page knows what to display 227 | var vm = await BuildLogoutViewModelAsync(logoutId); 228 | 229 | if (vm.ShowLogoutPrompt == false) 230 | { 231 | // if the request for logout was properly authenticated from IdentityServer, then 232 | // we don't need to show the prompt and can just log the user out directly. 233 | return await Logout(vm); 234 | } 235 | 236 | return View(vm); 237 | } 238 | 239 | /// 240 | /// Handle logout page postback 241 | /// 242 | [HttpPost] 243 | [ValidateAntiForgeryToken] 244 | public async Task Logout(LogoutInputModel model) 245 | { 246 | // build a model so the logged out page knows what to display 247 | var vm = await BuildLoggedOutViewModelAsync(model.LogoutId); 248 | 249 | if (User?.Identity.IsAuthenticated == true) 250 | { 251 | // delete local authentication cookie 252 | await HttpContext.SignOutAsync(); 253 | 254 | // raise the logout event 255 | await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); 256 | } 257 | 258 | // check if we need to trigger sign-out at an upstream identity provider 259 | if (vm.TriggerExternalSignout) 260 | { 261 | // build a return URL so the upstream provider will redirect back 262 | // to us after the user has logged out. this allows us to then 263 | // complete our single sign-out processing. 264 | string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); 265 | 266 | // this triggers a redirect to the external provider for sign-out 267 | return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); 268 | } 269 | 270 | return View("LoggedOut", vm); 271 | } 272 | 273 | /*****************************************/ 274 | /* helper APIs for the AccountController */ 275 | /*****************************************/ 276 | private async Task BuildLoginViewModelAsync(string returnUrl) 277 | { 278 | var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 279 | if (context?.IdP != null) 280 | { 281 | // this is meant to short circuit the UI and only trigger the one external IdP 282 | return new LoginViewModel 283 | { 284 | EnableLocalLogin = false, 285 | ReturnUrl = returnUrl, 286 | Username = context?.LoginHint, 287 | ExternalProviders = new ExternalProvider[] { new ExternalProvider { AuthenticationScheme = context.IdP } } 288 | }; 289 | } 290 | 291 | var schemes = await _schemeProvider.GetAllSchemesAsync(); 292 | 293 | var providers = schemes 294 | .Where(x => x.DisplayName != null || 295 | (x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) 296 | ) 297 | .Select(x => new ExternalProvider 298 | { 299 | DisplayName = x.DisplayName, 300 | AuthenticationScheme = x.Name 301 | }).ToList(); 302 | 303 | var allowLocal = true; 304 | if (context?.ClientId != null) 305 | { 306 | var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); 307 | if (client != null) 308 | { 309 | allowLocal = client.EnableLocalLogin; 310 | 311 | if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) 312 | { 313 | providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); 314 | } 315 | } 316 | } 317 | 318 | return new LoginViewModel 319 | { 320 | AllowRememberLogin = AccountOptions.AllowRememberLogin, 321 | EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, 322 | ReturnUrl = returnUrl, 323 | Username = context?.LoginHint, 324 | ExternalProviders = providers.ToArray() 325 | }; 326 | } 327 | 328 | private async Task BuildLoginViewModelAsync(LoginInputModel model) 329 | { 330 | var vm = await BuildLoginViewModelAsync(model.ReturnUrl); 331 | vm.Username = model.Username; 332 | vm.RememberLogin = model.RememberLogin; 333 | return vm; 334 | } 335 | 336 | private async Task BuildLogoutViewModelAsync(string logoutId) 337 | { 338 | var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt }; 339 | 340 | if (User?.Identity.IsAuthenticated != true) 341 | { 342 | // if the user is not authenticated, then just show logged out page 343 | vm.ShowLogoutPrompt = false; 344 | return vm; 345 | } 346 | 347 | var context = await _interaction.GetLogoutContextAsync(logoutId); 348 | if (context?.ShowSignoutPrompt == false) 349 | { 350 | // it's safe to automatically sign-out 351 | vm.ShowLogoutPrompt = false; 352 | return vm; 353 | } 354 | 355 | // show the logout prompt. this prevents attacks where the user 356 | // is automatically signed out by another malicious web page. 357 | return vm; 358 | } 359 | 360 | private async Task BuildLoggedOutViewModelAsync(string logoutId) 361 | { 362 | // get context information (client name, post logout redirect URI and iframe for federated signout) 363 | var logout = await _interaction.GetLogoutContextAsync(logoutId); 364 | 365 | var vm = new LoggedOutViewModel 366 | { 367 | AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, 368 | PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, 369 | ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName, 370 | SignOutIframeUrl = logout?.SignOutIFrameUrl, 371 | LogoutId = logoutId 372 | }; 373 | 374 | if (User?.Identity.IsAuthenticated == true) 375 | { 376 | var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; 377 | if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider) 378 | { 379 | var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp); 380 | if (providerSupportsSignout) 381 | { 382 | if (vm.LogoutId == null) 383 | { 384 | // if there's no current logout context, we need to create one 385 | // this captures necessary info from the current logged in user 386 | // before we signout and redirect away to the external IdP for signout 387 | vm.LogoutId = await _interaction.CreateLogoutContextAsync(); 388 | } 389 | 390 | vm.ExternalAuthenticationScheme = idp; 391 | } 392 | } 393 | } 394 | 395 | return vm; 396 | } 397 | 398 | private async Task ProcessWindowsLoginAsync(string returnUrl) 399 | { 400 | // see if windows auth has already been requested and succeeded 401 | var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName); 402 | if (result?.Principal is WindowsPrincipal wp) 403 | { 404 | // we will issue the external cookie and then redirect the 405 | // user back to the external callback, in essence, tresting windows 406 | // auth the same as any other external authentication mechanism 407 | var props = new AuthenticationProperties() 408 | { 409 | RedirectUri = Url.Action("ExternalLoginCallback"), 410 | Items = 411 | { 412 | { "returnUrl", returnUrl }, 413 | { "scheme", AccountOptions.WindowsAuthenticationSchemeName }, 414 | } 415 | }; 416 | 417 | var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName); 418 | id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name)); 419 | id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); 420 | 421 | // add the groups as claims -- be careful if the number of groups is too large 422 | if (AccountOptions.IncludeWindowsGroups) 423 | { 424 | var wi = wp.Identity as WindowsIdentity; 425 | var groups = wi.Groups.Translate(typeof(NTAccount)); 426 | var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value)); 427 | id.AddClaims(roles); 428 | } 429 | 430 | await HttpContext.SignInAsync( 431 | IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme, 432 | new ClaimsPrincipal(id), 433 | props); 434 | return Redirect(props.RedirectUri); 435 | } 436 | else 437 | { 438 | // trigger windows auth 439 | // since windows auth don't support the redirect uri, 440 | // this URL is re-triggered when we call challenge 441 | return Challenge(AccountOptions.WindowsAuthenticationSchemeName); 442 | } 443 | } 444 | 445 | private (TestUser user, string provider, string providerUserId, IEnumerable claims) FindUserFromExternalProvider(AuthenticateResult result) 446 | { 447 | var externalUser = result.Principal; 448 | 449 | // try to determine the unique id of the external user (issued by the provider) 450 | // the most common claim type for that are the sub claim and the NameIdentifier 451 | // depending on the external provider, some other claim type might be used 452 | var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ?? 453 | externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? 454 | throw new Exception("Unknown userid"); 455 | 456 | // remove the user id claim so we don't include it as an extra claim if/when we provision the user 457 | var claims = externalUser.Claims.ToList(); 458 | claims.Remove(userIdClaim); 459 | 460 | var provider = result.Properties.Items["scheme"]; 461 | var providerUserId = userIdClaim.Value; 462 | 463 | // find external user 464 | var user = _users.FindByExternalProvider(provider, providerUserId); 465 | 466 | return (user, provider, providerUserId, claims); 467 | } 468 | 469 | private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable claims) 470 | { 471 | var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); 472 | return user; 473 | } 474 | 475 | private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 476 | { 477 | // if the external system sent a session id claim, copy it over 478 | // so we can use it for single sign-out 479 | var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); 480 | if (sid != null) 481 | { 482 | localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); 483 | } 484 | 485 | // if the external provider issued an id_token, we'll keep it for signout 486 | var id_token = externalResult.Properties.GetTokenValue("id_token"); 487 | if (id_token != null) 488 | { 489 | localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); 490 | } 491 | } 492 | 493 | private void ProcessLoginCallbackForWsFed(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 494 | { 495 | } 496 | 497 | private void ProcessLoginCallbackForSaml2p(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 498 | { 499 | } 500 | } 501 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 | // specify the Windows authentication scheme being used 19 | public static readonly string WindowsAuthenticationSchemeName = Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme; 20 | // if user uses windows auth, should we load the groups from windows 21 | public static bool IncludeWindowsGroups = false; 22 | 23 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.Quickstart.UI 6 | { 7 | public class ExternalProvider 8 | { 9 | public string DisplayName { get; set; } 10 | public string AuthenticationScheme { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.Quickstart.UI 10 | { 11 | public class LoginViewModel : LoginInputModel 12 | { 13 | public bool AllowRememberLogin { get; set; } 14 | public bool EnableLocalLogin { get; set; } 15 | 16 | public IEnumerable ExternalProviders { get; set; } 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 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.Quickstart.UI 6 | { 7 | public class LogoutInputModel 8 | { 9 | public string LogoutId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.Quickstart.UI 6 | { 7 | public class LogoutViewModel : LogoutInputModel 8 | { 9 | public bool ShowLogoutPrompt { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Quickstart/Consent/ConsentController.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 IdentityServer4.Services; 7 | using IdentityServer4.Stores; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Logging; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace IdentityServer4.Quickstart.UI 15 | { 16 | /// 17 | /// This controller processes the consent UI 18 | /// 19 | [SecurityHeaders] 20 | [Authorize] 21 | public class ConsentController : Controller 22 | { 23 | private readonly IIdentityServerInteractionService _interaction; 24 | private readonly IClientStore _clientStore; 25 | private readonly IResourceStore _resourceStore; 26 | private readonly ILogger _logger; 27 | 28 | public ConsentController( 29 | IIdentityServerInteractionService interaction, 30 | IClientStore clientStore, 31 | IResourceStore resourceStore, 32 | ILogger logger) 33 | { 34 | _interaction = interaction; 35 | _clientStore = clientStore; 36 | _resourceStore = resourceStore; 37 | _logger = logger; 38 | } 39 | 40 | /// 41 | /// Shows the consent screen 42 | /// 43 | /// 44 | /// 45 | [HttpGet] 46 | public async Task Index(string returnUrl) 47 | { 48 | var vm = await BuildViewModelAsync(returnUrl); 49 | if (vm != null) 50 | { 51 | return View("Index", vm); 52 | } 53 | 54 | return View("Error"); 55 | } 56 | 57 | /// 58 | /// Handles the consent screen postback 59 | /// 60 | [HttpPost] 61 | [ValidateAntiForgeryToken] 62 | public async Task Index(ConsentInputModel model) 63 | { 64 | var result = await ProcessConsent(model); 65 | 66 | if (result.IsRedirect) 67 | { 68 | return Redirect(result.RedirectUri); 69 | } 70 | 71 | if (result.HasValidationError) 72 | { 73 | ModelState.AddModelError("", result.ValidationError); 74 | } 75 | 76 | if (result.ShowView) 77 | { 78 | return View("Index", result.ViewModel); 79 | } 80 | 81 | return View("Error"); 82 | } 83 | 84 | /*****************************************/ 85 | /* helper APIs for the ConsentController */ 86 | /*****************************************/ 87 | private async Task ProcessConsent(ConsentInputModel model) 88 | { 89 | var result = new ProcessConsentResult(); 90 | 91 | ConsentResponse grantedConsent = null; 92 | 93 | // user clicked 'no' - send back the standard 'access_denied' response 94 | if (model.Button == "no") 95 | { 96 | grantedConsent = ConsentResponse.Denied; 97 | } 98 | // user clicked 'yes' - validate the data 99 | else if (model.Button == "yes" && model != null) 100 | { 101 | // if the user consented to some scope, build the response model 102 | if (model.ScopesConsented != null && model.ScopesConsented.Any()) 103 | { 104 | var scopes = model.ScopesConsented; 105 | if (ConsentOptions.EnableOfflineAccess == false) 106 | { 107 | scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); 108 | } 109 | 110 | grantedConsent = new ConsentResponse 111 | { 112 | RememberConsent = model.RememberConsent, 113 | ScopesConsented = scopes.ToArray() 114 | }; 115 | } 116 | else 117 | { 118 | result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; 119 | } 120 | } 121 | else 122 | { 123 | result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; 124 | } 125 | 126 | if (grantedConsent != null) 127 | { 128 | // validate return url is still valid 129 | var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); 130 | if (request == null) return result; 131 | 132 | // communicate outcome of consent back to identityserver 133 | await _interaction.GrantConsentAsync(request, grantedConsent); 134 | 135 | // indicate that's it ok to redirect back to authorization endpoint 136 | result.RedirectUri = model.ReturnUrl; 137 | } 138 | else 139 | { 140 | // we need to redisplay the consent UI 141 | result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model); 142 | } 143 | 144 | return result; 145 | } 146 | 147 | private async Task BuildViewModelAsync(string returnUrl, ConsentInputModel model = null) 148 | { 149 | var request = await _interaction.GetAuthorizationContextAsync(returnUrl); 150 | if (request != null) 151 | { 152 | var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); 153 | if (client != null) 154 | { 155 | var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); 156 | if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) 157 | { 158 | return CreateConsentViewModel(model, returnUrl, request, client, resources); 159 | } 160 | else 161 | { 162 | _logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y)); 163 | } 164 | } 165 | else 166 | { 167 | _logger.LogError("Invalid client id: {0}", request.ClientId); 168 | } 169 | } 170 | else 171 | { 172 | _logger.LogError("No consent request matching request: {0}", returnUrl); 173 | } 174 | 175 | return null; 176 | } 177 | 178 | private ConsentViewModel CreateConsentViewModel( 179 | ConsentInputModel model, string returnUrl, 180 | AuthorizationRequest request, 181 | Client client, Resources resources) 182 | { 183 | var vm = new ConsentViewModel(); 184 | vm.RememberConsent = model?.RememberConsent ?? true; 185 | vm.ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(); 186 | 187 | vm.ReturnUrl = returnUrl; 188 | 189 | vm.ClientName = client.ClientName ?? client.ClientId; 190 | vm.ClientUrl = client.ClientUri; 191 | vm.ClientLogoUrl = client.LogoUri; 192 | vm.AllowRememberConsent = client.AllowRememberConsent; 193 | 194 | vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 195 | vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 196 | if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) 197 | { 198 | vm.ResourceScopes = vm.ResourceScopes.Union(new ScopeViewModel[] { 199 | GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) 200 | }); 201 | } 202 | 203 | return vm; 204 | } 205 | 206 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 207 | { 208 | return new ScopeViewModel 209 | { 210 | Name = identity.Name, 211 | DisplayName = identity.DisplayName, 212 | Description = identity.Description, 213 | Emphasize = identity.Emphasize, 214 | Required = identity.Required, 215 | Checked = check || identity.Required 216 | }; 217 | } 218 | 219 | public ScopeViewModel CreateScopeViewModel(Scope scope, bool check) 220 | { 221 | return new ScopeViewModel 222 | { 223 | Name = scope.Name, 224 | DisplayName = scope.DisplayName, 225 | Description = scope.Description, 226 | Emphasize = scope.Emphasize, 227 | Required = scope.Required, 228 | Checked = check || scope.Required 229 | }; 230 | } 231 | 232 | private ScopeViewModel GetOfflineAccessScope(bool check) 233 | { 234 | return new ScopeViewModel 235 | { 236 | Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, 237 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 238 | Description = ConsentOptions.OfflineAccessDescription, 239 | Emphasize = true, 240 | Checked = check 241 | }; 242 | } 243 | } 244 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 | } 16 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 ResourceScopes { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ProcessConsentResult 8 | { 9 | public bool IsRedirect => RedirectUri != null; 10 | public string RedirectUri { get; set; } 11 | 12 | public bool ShowView => ViewModel != null; 13 | public ConsentViewModel ViewModel { get; set; } 14 | 15 | public bool HasValidationError => ValidationError != null; 16 | public string ValidationError { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.Quickstart.UI 6 | { 7 | public class ScopeViewModel 8 | { 9 | public string Name { 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 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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; 9 | using System.Collections.Generic; 10 | using System.Text; 11 | 12 | namespace IdentityServer4.Quickstart.UI 13 | { 14 | public class DiagnosticsViewModel 15 | { 16 | public DiagnosticsViewModel(AuthenticateResult result) 17 | { 18 | AuthenticateResult = result; 19 | 20 | if (result.Properties.Items.ContainsKey("client_list")) 21 | { 22 | var encoded = result.Properties.Items["client_list"]; 23 | var bytes = Base64Url.Decode(encoded); 24 | var value = Encoding.UTF8.GetString(bytes); 25 | 26 | Clients = JsonConvert.DeserializeObject(value); 27 | } 28 | } 29 | 30 | public AuthenticateResult AuthenticateResult { get; } 31 | public IEnumerable Clients { get; } = new List(); 32 | } 33 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | 13 | namespace IdentityServer4.Quickstart.UI 14 | { 15 | /// 16 | /// This sample controller allows a user to revoke grants given to clients 17 | /// 18 | [SecurityHeaders] 19 | [Authorize] 20 | public class GrantsController : Controller 21 | { 22 | private readonly IIdentityServerInteractionService _interaction; 23 | private readonly IClientStore _clients; 24 | private readonly IResourceStore _resources; 25 | 26 | public GrantsController(IIdentityServerInteractionService interaction, 27 | IClientStore clients, 28 | IResourceStore resources) 29 | { 30 | _interaction = interaction; 31 | _clients = clients; 32 | _resources = resources; 33 | } 34 | 35 | /// 36 | /// Show list of grants 37 | /// 38 | [HttpGet] 39 | public async Task Index() 40 | { 41 | return View("Index", await BuildViewModelAsync()); 42 | } 43 | 44 | /// 45 | /// Handle postback to revoke a client 46 | /// 47 | [HttpPost] 48 | [ValidateAntiForgeryToken] 49 | public async Task Revoke(string clientId) 50 | { 51 | await _interaction.RevokeUserConsentAsync(clientId); 52 | return RedirectToAction("Index"); 53 | } 54 | 55 | private async Task BuildViewModelAsync() 56 | { 57 | var grants = await _interaction.GetAllUserConsentsAsync(); 58 | 59 | var list = new List(); 60 | foreach(var grant in grants) 61 | { 62 | var client = await _clients.FindClientByIdAsync(grant.ClientId); 63 | if (client != null) 64 | { 65 | var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); 66 | 67 | var item = new GrantViewModel() 68 | { 69 | ClientId = client.ClientId, 70 | ClientName = client.ClientName ?? client.ClientId, 71 | ClientLogoUrl = client.LogoUri, 72 | ClientUrl = client.ClientUri, 73 | Created = grant.CreationTime, 74 | Expires = grant.Expiration, 75 | IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), 76 | ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray() 77 | }; 78 | 79 | list.Add(item); 80 | } 81 | } 82 | 83 | return new GrantsViewModel 84 | { 85 | Grants = list 86 | }; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 DateTime Created { get; set; } 22 | public DateTime? Expires { get; set; } 23 | public IEnumerable IdentityGrantNames { get; set; } 24 | public IEnumerable ApiGrantNames { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.Quickstart.UI 8 | { 9 | public class ErrorViewModel 10 | { 11 | public ErrorMessage Error { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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.Mvc; 7 | using System.Threading.Tasks; 8 | 9 | namespace IdentityServer4.Quickstart.UI 10 | { 11 | [SecurityHeaders] 12 | public class HomeController : Controller 13 | { 14 | private readonly IIdentityServerInteractionService _interaction; 15 | 16 | public HomeController(IIdentityServerInteractionService interaction) 17 | { 18 | _interaction = interaction; 19 | } 20 | 21 | public IActionResult Index() 22 | { 23 | return View(); 24 | } 25 | 26 | /// 27 | /// Shows the error page 28 | /// 29 | public async Task Error(string errorId) 30 | { 31 | var vm = new ErrorViewModel(); 32 | 33 | // retrieve error details from identityserver 34 | var message = await _interaction.GetErrorContextAsync(errorId); 35 | if (message != null) 36 | { 37 | vm.Error = message; 38 | } 39 | 40 | return View("Error", vm); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 IdentityServer4.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 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | 10 | namespace IdentityServer4.Quickstart.UI 11 | { 12 | public class TestUsers 13 | { 14 | public static List Users = new List 15 | { 16 | new TestUser{SubjectId = "818727", Username = "alice", Password = "alice", 17 | Claims = 18 | { 19 | new Claim(JwtClaimTypes.Name, "Alice Smith"), 20 | new Claim(JwtClaimTypes.GivenName, "Alice"), 21 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 22 | new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), 23 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 24 | new Claim(JwtClaimTypes.WebSite, "http://alice.com"), 25 | new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) 26 | } 27 | }, 28 | new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob", 29 | Claims = 30 | { 31 | new Claim(JwtClaimTypes.Name, "Bob Smith"), 32 | new Claim(JwtClaimTypes.GivenName, "Bob"), 33 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 34 | new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), 35 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 36 | new Claim(JwtClaimTypes.WebSite, "http://bob.com"), 37 | new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), 38 | new Claim("location", "somewhere") 39 | } 40 | } 41 | }; 42 | } 43 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/QuickstartIdentityServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | using IdentityServer4; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.IdentityModel.Tokens; 9 | 10 | namespace QuickstartIdentityServer 11 | { 12 | public class Startup 13 | { 14 | public void ConfigureServices(IServiceCollection services) 15 | { 16 | services.AddMvc(); 17 | 18 | // configure identity server with in-memory stores, keys, clients and scopes 19 | services.AddIdentityServer() 20 | .AddDeveloperSigningCredential() 21 | .AddInMemoryIdentityResources(Config.GetIdentityResources()) 22 | .AddInMemoryApiResources(Config.GetApiResources()) 23 | .AddInMemoryClients(Config.GetClients()) 24 | .AddTestUsers(Config.GetUsers()); 25 | 26 | services.AddAuthentication() 27 | .AddGoogle("Google", options => 28 | { 29 | options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; 30 | 31 | options.ClientId = "1005450161824-hqg5jq2qnplaskfnjcor4erfb5m8g0rs.apps.googleusercontent.com"; 32 | options.ClientSecret = "GPj_fCos-5jC1dhqNL3yCZjE"; 33 | }); 34 | } 35 | 36 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 37 | { 38 | if (env.IsDevelopment()) 39 | { 40 | app.UseDeveloperExceptionPage(); 41 | } 42 | 43 | app.UseIdentityServer(); 44 | 45 | app.UseStaticFiles(); 46 | app.UseMvcWithDefaultRoute(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | 27 | 28 | @section scripts 29 | { 30 | @if (Model.AutomaticRedirectAfterSignOut) 31 | { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model LoginViewModel 2 | 3 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @model LogoutViewModel 2 | 3 |
4 | 7 | 8 |
9 |
10 |

Would you like to logout of IdentityServer?

11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |
-------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Consent/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model ConsentViewModel 2 | 3 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Consent/_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 |
  • -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Diagnostics/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model DiagnosticsViewModel 2 | 3 |

    Authentication cookie

    4 | 5 |

    Claims

    6 |
    7 | @foreach (var claim in Model.AuthenticateResult.Principal.Claims) 8 | { 9 |
    @claim.Type
    10 |
    @claim.Value
    11 | } 12 |
    13 | 14 |

    Properties

    15 |
    16 | @foreach (var prop in Model.AuthenticateResult.Properties.Items) 17 | { 18 |
    @prop.Key
    19 |
    @prop.Value
    20 | } 21 |
    22 | 23 | @if (Model.Clients.Any()) 24 | { 25 |

    Clients

    26 |
      27 | @foreach (var client in Model.Clients) 28 | { 29 |
    • @client
    • 30 | } 31 |
    32 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Grants/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model GrantsViewModel 2 | 3 |
    4 | 12 | 13 | @if (Model.Grants.Any() == false) 14 | { 15 |
    16 |
    17 |
    18 | You have not given access to any applications 19 |
    20 |
    21 |
    22 | } 23 | else 24 | { 25 | foreach (var grant in Model.Grants) 26 | { 27 |
    28 |
    29 | @if (grant.ClientLogoUrl != null) 30 | { 31 | 32 | } 33 |
    34 |
    35 |
    @grant.ClientName
    36 |
    37 | Created: @grant.Created.ToString("yyyy-MM-dd") 38 |
    39 | @if (grant.Expires.HasValue) 40 | { 41 |
    42 | Expires: @grant.Expires.Value.ToString("yyyy-MM-dd") 43 |
    44 | } 45 | @if (grant.IdentityGrantNames.Any()) 46 | { 47 |
    48 |
    Identity Grants
    49 |
      50 | @foreach (var name in grant.IdentityGrantNames) 51 | { 52 |
    • @name
    • 53 | } 54 |
    55 |
    56 | } 57 | @if (grant.ApiGrantNames.Any()) 58 | { 59 |
    60 |
    API Grants
    61 |
      62 | @foreach (var name in grant.ApiGrantNames) 63 | { 64 |
    • @name
    • 65 | } 66 |
    67 |
    68 | } 69 |
    70 |
    71 |
    72 | 73 | 74 |
    75 |
    76 |
    77 | } 78 | } 79 |
    -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | 
    2 | 11 | 12 |
    13 |
    14 |

    15 | IdentityServer publishes a 16 | discovery document 17 | where you can find metadata and links to all the endpoints, key material, etc. 18 |

    19 |
    20 |
    21 |

    22 | Click here to manage your stored grants. 23 |

    24 |
    25 |
    26 |
    27 |
    28 |

    29 | Here are links to the 30 | source code repository, 31 | and ready to use samples. 32 |

    33 |
    34 |
    35 |
    36 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Hosting 2 | @model ErrorViewModel 3 | @inject IHostingEnvironment host 4 | 5 | @{ 6 | var error = Model?.Error?.Error; 7 | var errorDescription = host.IsDevelopment() ? Model?.Error?.ErrorDescription : null; 8 | var request_id = Model?.Error?.RequestId; 9 | } 10 | 11 |
    12 | 15 | 16 |
    17 |
    18 |
    19 | Sorry, there was an error 20 | 21 | @if (error != null) 22 | { 23 | 24 | 25 | : @error 26 | 27 | 28 | 29 | if (errorDescription != null) 30 | { 31 |
    @errorDescription
    32 | } 33 | } 34 |
    35 | 36 | @if (request_id != null) 37 | { 38 |
    Request Id: @request_id
    39 | } 40 |
    41 |
    42 |
    43 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Extensions 2 | @{ 3 | string name = null; 4 | if (!true.Equals(ViewData["signed-out"])) 5 | { 6 | name = Context.User?.GetDisplayName(); 7 | } 8 | } 9 | 10 | 11 | 12 | 13 | 14 | 15 | IdentityServer4 16 | 17 | 18 | 19 | 20 | 21 | 22 | 52 | 53 |
    54 | @RenderBody() 55 |
    56 | 57 | 58 | 59 | @RenderSection("scripts", required: false) 60 | 61 | 62 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Quickstart.UI 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/tempkey.rsa: -------------------------------------------------------------------------------- 1 | {"KeyId":"46bc7b8ca2e44aaa0396c937e76c3683","Parameters":{"D":"UwuiW7ZSD2FxT2yRwylUGogCy0UWJFcDK0zqPTh6KcpRz+1MKoPnsrvNujzWDV+8zk1GN4AbAHNs62XBBIUAFMwb7wOYuunaclsCZbbHnxgkT0Zg9fbRObuddRXP+SJm0Iyi6efvVEhcihkcY8A9jhG5ue466LYdBvs29UIPTtojP1OQAN8aiyYYyir/HM5QUqtQpvFXPv2ocaiBowIq+HFN0wuzE873l6jamQ45D958KA0yTOIz81/xg3bTVMMNZKv53aZcNfKu66G3qChGVUzGkMkAPCW6JvWoPpBmSwmemVHN5Lqunm8aUWUWyACMGO7qnQL0O1wOVC9xfTz9mQ==","DP":"qqzeVzMxdnPy0SDcwJOGigUq17oE4pw4r9SWEd5u+3FvMjCU5ZIYtokUgKVuCMu2q7Snsd4bqg07yDC/CvDNYg0dWVyrZL3ErFBUcX0i9wxklG4hrOe80XOH3KSDWSg+eYpXCx3q0moNIFVYG+eraO8cgeFJqKoVp/I9V60qK3M=","DQ":"kKvtmbQldZBVjq+f/yG2THSGkS1Tdqu9EQXg9TNeAG+7TfiRObtpuXKKeedTNF4HtwkdcCVg67U2KUXNSVg7b5YcBuUJ9EpAsBRU50txfwpAcnz88piGXGPpr+nhK8lUPPEzX7/+SnTEuMpe56oyHBsbN+Xpa21w+0rnHVp593E=","Exponent":"AQAB","InverseQ":"4RaloT61wnVIbAXuLiMgzHWT37GqdU51ToFVbcGRoIFnSu8kLixNbVTyMbGu4+KRMAZ1Ndc0/t1V7XFOjYhwe6APa69VwyM3752nqgtevnFtcn8VkQCAWt4x3iwt6VhMYNzO/Cjo7GMTwGo5OBYfdWm07OcqO5SbKnsCalYE6KA=","Modulus":"yBQ6avQtc+Id73RGivli8lriwy888NJGwWEn1DUl32eTFBzMe2YOQR60CMHyNJF6YcgkCGqh1onxC0y0QyOF5ho+tQbnwwpiQOmXBFJD7Y1UcxD2AjgLcvRqsMVNvo8V4Vo1oSAkGlkcnS4xstXLUdnKA805yvgEVuKjCrgpREnX4AO5KUiBI979TeMueYiBOWoozVsc/3n1fz1cvD6BVSYK9I2SyrhisxbKuQhWw0Zjfa8s0KAcG9fKf5fpORs5lYkBdpk/5EhzDQrqLrUxiIdSZZ9AIm4CypoRF8iDiuJEJvY0JHtXB8df76l0iE3juCYVU7aW3VEofnZ3n58ZxQ==","P":"8n6ZRwIhGPznl4yYRl/MIM8xhtQQm9jhZIKWphx0F45aUSCLiUsTM1oekTqxpGziz1RIbdTRuGZ2E4y2nKw0NQEXCk5m6fqaLTFcAr1D2Ta5txQNZzgf4pwU2KuDjvUdV1IuN200dwn7UWn9IdB0rzDdHQIW+e67AmnjVwIuJS8=","Q":"0zjiVJkckh+LYLVODoS1HpWqiCqMdJyySGCk82L6aOx+dV5kkeN1a4QbFK2hiEm1BZ3XWwmKqNii9nEBC2RbHIYz515m+4HUn85jhzAXyNx0mRoMaFRGMuJsm8hvcp+YYGPreW0rLMZ2bX54Rzn0FRAOV+EqTTxupN0zvWWl20s="}} -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 65px; 3 | } 4 | .navbar-header { 5 | position: relative; 6 | top: -4px; 7 | } 8 | .navbar-brand > .icon-banner { 9 | position: relative; 10 | top: -2px; 11 | display: inline; 12 | } 13 | .icon { 14 | position: relative; 15 | top: -10px; 16 | } 17 | .logged-out iframe { 18 | display: none; 19 | width: 0; 20 | height: 0; 21 | } 22 | .page-consent .client-logo { 23 | float: left; 24 | } 25 | .page-consent .client-logo img { 26 | width: 80px; 27 | height: 80px; 28 | } 29 | .page-consent .consent-buttons { 30 | margin-top: 25px; 31 | } 32 | .page-consent .consent-form .consent-scopecheck { 33 | display: inline-block; 34 | margin-right: 5px; 35 | } 36 | .page-consent .consent-form .consent-description { 37 | margin-left: 25px; 38 | } 39 | .page-consent .consent-form .consent-description label { 40 | font-weight: normal; 41 | } 42 | .page-consent .consent-form .consent-remember { 43 | padding-left: 16px; 44 | } 45 | .grants .page-header { 46 | margin-bottom: 10px; 47 | } 48 | .grants .grant { 49 | margin-top: 20px; 50 | padding-bottom: 20px; 51 | border-bottom: 1px solid lightgray; 52 | } 53 | .grants .grant img { 54 | width: 100px; 55 | height: 100px; 56 | } 57 | .grants .grant .clientname { 58 | font-size: 140%; 59 | font-weight: bold; 60 | } 61 | .grants .grant .granttype { 62 | font-size: 120%; 63 | font-weight: bold; 64 | } 65 | .grants .grant .created { 66 | font-size: 120%; 67 | font-weight: bold; 68 | } 69 | .grants .grant .expires { 70 | font-size: 120%; 71 | font-weight: bold; 72 | } 73 | .grants .grant li { 74 | list-style-type: none; 75 | display: inline; 76 | } 77 | .grants .grant li:after { 78 | content: ', '; 79 | } 80 | .grants .grant li:last-child:after { 81 | content: ''; 82 | } -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/css/site.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 65px; 3 | } 4 | 5 | .navbar-header { 6 | position: relative; 7 | top: -4px; 8 | } 9 | 10 | .navbar-brand > .icon-banner { 11 | position: relative; 12 | top: -2px; 13 | display: inline; 14 | } 15 | 16 | .icon { 17 | position: relative; 18 | top: -10px; 19 | } 20 | 21 | .logged-out iframe { 22 | display: none; 23 | width: 0; 24 | height: 0; 25 | } 26 | 27 | .page-consent { 28 | .client-logo { 29 | float: left; 30 | 31 | img { 32 | width: 80px; 33 | height: 80px; 34 | } 35 | } 36 | 37 | .consent-buttons { 38 | margin-top: 25px; 39 | } 40 | 41 | .consent-form { 42 | .consent-scopecheck { 43 | display: inline-block; 44 | margin-right: 5px; 45 | } 46 | 47 | .consent-scopecheck[disabled] { 48 | //visibility:hidden; 49 | } 50 | 51 | .consent-description { 52 | margin-left: 25px; 53 | 54 | label { 55 | font-weight: normal; 56 | } 57 | } 58 | 59 | .consent-remember { 60 | padding-left: 16px; 61 | } 62 | } 63 | } 64 | 65 | .grants { 66 | .page-header { 67 | margin-bottom: 10px; 68 | } 69 | 70 | .grant { 71 | margin-top: 20px; 72 | padding-bottom: 20px; 73 | border-bottom: 1px solid lightgray; 74 | 75 | img { 76 | width: 100px; 77 | height: 100px; 78 | } 79 | 80 | .clientname { 81 | font-size: 140%; 82 | font-weight: bold; 83 | } 84 | 85 | .granttype { 86 | font-size: 120%; 87 | font-weight: bold; 88 | } 89 | 90 | .created { 91 | font-size: 120%; 92 | font-weight: bold; 93 | } 94 | 95 | .expires { 96 | font-size: 120%; 97 | font-weight: bold; 98 | } 99 | 100 | li { 101 | list-style-type: none; 102 | display: inline; 103 | 104 | &:after { 105 | content: ', '; 106 | } 107 | 108 | &:last-child:after { 109 | content: ''; 110 | } 111 | 112 | .displayname { 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{margin-top:65px;}.navbar-header{position:relative;top:-4px;}.navbar-brand>.icon-banner{position:relative;top:-2px;display:inline;}.icon{position:relative;top:-10px;}.logged-out iframe{display:none;width:0;height:0;}.page-consent .client-logo{float:left;}.page-consent .client-logo img{width:80px;height:80px;}.page-consent .consent-buttons{margin-top:25px;}.page-consent .consent-form .consent-scopecheck{display:inline-block;margin-right:5px;}.page-consent .consent-form .consent-description{margin-left:25px;}.page-consent .consent-form .consent-description label{font-weight:normal;}.page-consent .consent-form .consent-remember{padding-left:16px;}.grants .page-header{margin-bottom:10px;}.grants .grant{margin-top:20px;padding-bottom:20px;border-bottom:1px solid #d3d3d3;}.grants .grant img{width:100px;height:100px;}.grants .grant .clientname{font-size:140%;font-weight:bold;}.grants .grant .granttype{font-size:120%;font-weight:bold;}.grants .grant .created{font-size:120%;font-weight:bold;}.grants .grant .expires{font-size:120%;font-weight:bold;}.grants .grant li{list-style-type:none;display:inline;}.grants .grant li:after{content:', ';}.grants .grant li:last-child:after{content:'';} -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/QuickstartIdentityServer/wwwroot/favicon.ico -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/QuickstartIdentityServer/wwwroot/icon.jpg -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/QuickstartIdentityServer/wwwroot/icon.png -------------------------------------------------------------------------------- /QuickstartIdentityServer/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 | -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/QuickstartIdentityServer/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /QuickstartIdentityServer/wwwroot/lib/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.5 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.5",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.5",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.5",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.5",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.5",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&jdocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth
    ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.5",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.5",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /VueApi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28302.56 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickstartIdentityServer", "QuickstartIdentityServer\QuickstartIdentityServer.csproj", "{6F5824E4-801F-4FBC-99E6-700DF9F31EF5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VueApi", "VueApi\VueApi.csproj", "{EF99C908-9D3E-4D13-BB5A-FA5B833D65B0}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {6F5824E4-801F-4FBC-99E6-700DF9F31EF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {6F5824E4-801F-4FBC-99E6-700DF9F31EF5}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {6F5824E4-801F-4FBC-99E6-700DF9F31EF5}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {6F5824E4-801F-4FBC-99E6-700DF9F31EF5}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {EF99C908-9D3E-4D13-BB5A-FA5B833D65B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {EF99C908-9D3E-4D13-BB5A-FA5B833D65B0}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {EF99C908-9D3E-4D13-BB5A-FA5B833D65B0}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {EF99C908-9D3E-4D13-BB5A-FA5B833D65B0}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {DE6E1A3D-3971-47E2-99B3-270E28F174BB} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /VueApi/Controllers/CatchAllController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace VueApi.Controllers 8 | { 9 | public class CatchAllController : Controller 10 | { 11 | public IActionResult Index() 12 | { 13 | return File("~/index.html", "text/html"); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /VueApi/Controllers/ServicesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | 9 | namespace VueApi.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | [Authorize] 14 | public class ServicesController : ControllerBase 15 | { 16 | [HttpGet] 17 | public ActionResult> Get() 18 | { 19 | return new ServiceDto[] { 20 | new ServiceDto() { 21 | Name ="Development", 22 | Uri="https://readify.net/services/development/", 23 | IconUri="https://readify.net/media/1187/development.png" 24 | }, 25 | new ServiceDto() { 26 | Name ="Innovation and Design", 27 | Uri="https://readify.net/services/innovation-design/", 28 | IconUri="https://readify.net/media/1189/light-bulb.png" 29 | }, 30 | new ServiceDto() { 31 | Name ="Data and Analytics", 32 | Uri="https://readify.net/services/data-analytics/", 33 | IconUri="https://readify.net/media/1184/data.png" 34 | }, 35 | new ServiceDto() { 36 | Name ="DevOps", 37 | Uri="https://readify.net/services/devops/", 38 | IconUri="https://readify.net/media/1188/devops.png" 39 | }, 40 | }; 41 | } 42 | } 43 | 44 | public class ServiceDto 45 | { 46 | public string Name { get; set; } 47 | public string Uri { get; set; } 48 | public string IconUri { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /VueApi/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace VueApi.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | [ApiController] 11 | public class ValuesController : ControllerBase 12 | { 13 | // GET api/values 14 | [HttpGet] 15 | public ActionResult> Get() 16 | { 17 | return new string[] { "value1", "value2" }; 18 | } 19 | 20 | // GET api/values/5 21 | [HttpGet("{id}")] 22 | public ActionResult Get(int id) 23 | { 24 | return "value"; 25 | } 26 | 27 | // POST api/values 28 | [HttpPost] 29 | public void Post([FromBody] string value) 30 | { 31 | } 32 | 33 | // PUT api/values/5 34 | [HttpPut("{id}")] 35 | public void Put(int id, [FromBody] string value) 36 | { 37 | } 38 | 39 | // DELETE api/values/5 40 | [HttpDelete("{id}")] 41 | public void Delete(int id) 42 | { 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /VueApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace VueApi 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /VueApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:20437", 7 | "sslPort": 44326 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "VueApi": { 21 | "commandName": "Project", 22 | "launchUrl": "api/values", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "https://localhost:5000" 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /VueApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IdentityModel.Tokens.Jwt; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authentication.JwtBearer; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.HttpsPolicy; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.Extensions.Options; 15 | 16 | namespace VueApi 17 | { 18 | public class Startup 19 | { 20 | public Startup(IConfiguration configuration) 21 | { 22 | Configuration = configuration; 23 | } 24 | 25 | public IConfiguration Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 31 | 32 | JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 33 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 34 | .AddIdentityServerAuthentication(options => 35 | { 36 | options.Authority = "https://localhost:5443/"; 37 | options.RequireHttpsMetadata = true; 38 | options.ApiName = "api1"; 39 | }); 40 | 41 | } 42 | 43 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 44 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 45 | { 46 | if (env.IsDevelopment()) 47 | { 48 | app.UseDeveloperExceptionPage(); 49 | } 50 | 51 | app.UseAuthentication(); 52 | 53 | app.UseDefaultFiles(); 54 | app.UseStaticFiles(); 55 | app.UseMvc(routes => 56 | { 57 | routes.MapSpaFallbackRoute( 58 | name: "spa-fallback", 59 | defaults: new { controller = "CatchAll", action = "Index" }); 60 | }); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /VueApi/VueApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | inprocess 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /VueApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /VueApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /VueApi/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/VueApi/wwwroot/favicon.ico -------------------------------------------------------------------------------- /VueApi/wwwroot/img/logo.82b9c7a5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/VueApi/wwwroot/img/logo.82b9c7a5.png -------------------------------------------------------------------------------- /vue-app/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /vue-app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /vue-app/README.md: -------------------------------------------------------------------------------- 1 | # vue-app 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | -------------------------------------------------------------------------------- /vue-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /vue-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.18.0", 11 | "oidc-client": "^1.5.4", 12 | "vue": "^2.5.17", 13 | "vue-router": "^3.0.1" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "^3.0.5", 17 | "@vue/cli-service": "^3.0.5", 18 | "vue-template-compiler": "^2.5.17" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vue-app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /vue-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/vue-app/public/favicon.ico -------------------------------------------------------------------------------- /vue-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-app 9 | 10 | 11 | 14 |
    15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vue-app/public/static/silent-renew.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Silent Renew Token 5 | 6 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /vue-app/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | -------------------------------------------------------------------------------- /vue-app/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbanks54/vue-and-identityserver/79ae5bc5f94038382c7ab2c946eaf25ff3cfb4b4/vue-app/src/assets/logo.png -------------------------------------------------------------------------------- /vue-app/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 42 | 58 | -------------------------------------------------------------------------------- /vue-app/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import mgr from './services/security.js' 5 | import axios from 'axios' 6 | 7 | Vue.config.productionTip = false 8 | 9 | const globalData = { 10 | isAuthenticated: false, 11 | user: '', 12 | mgr: mgr 13 | } 14 | 15 | const globalMethods = { 16 | async authenticate(returnPath) { 17 | const user = await this.$root.getUser(); //see if the user details are in local storage 18 | if (!!user) { 19 | this.isAuthenticated = true; 20 | this.user = user; 21 | } else { 22 | await this.$root.signIn(returnPath); 23 | } 24 | }, 25 | async getUser () { 26 | try { 27 | let user = await this.mgr.getUser(); 28 | return user; 29 | } catch (err) { 30 | console.log(err); 31 | } 32 | }, 33 | signIn (returnPath) { 34 | returnPath ? this.mgr.signinRedirect({ state: returnPath }) 35 | : this.mgr.signinRedirect(); 36 | } 37 | } 38 | 39 | let v = new Vue({ 40 | router, 41 | data: globalData, 42 | methods: globalMethods, 43 | render: h => h(App), 44 | }).$mount('#app') 45 | 46 | axios.interceptors.request.use((config) => { 47 | const user = v.$root.user; 48 | if (user) { 49 | const authToken = user.access_token; 50 | if (authToken) { 51 | config.headers.Authorization = `Bearer ${authToken}`; 52 | } 53 | } 54 | return config; 55 | }, 56 | (err) => { 57 | //What do we do when we get errors? 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /vue-app/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './views/Home.vue' 4 | import Callback from './views/Callback' 5 | import Unauthorized from './views/Unauthorized' 6 | 7 | Vue.use(Router) 8 | 9 | let router = new Router({ 10 | mode: 'history', 11 | base: process.env.BASE_URL, 12 | routes: [ 13 | { 14 | path: '/', 15 | name: 'home', 16 | component: Home 17 | }, 18 | { 19 | path: '/about', 20 | name: 'about', 21 | meta: { 22 | requiresAuth: true 23 | }, 24 | // route level code-splitting 25 | // this generates a separate chunk (about.[hash].js) for this route 26 | // which is lazy-loaded when the route is visited. 27 | component: () => import(/* webpackChunkName: "about" */ './views/About.vue') 28 | }, 29 | { 30 | path: '/callback', 31 | name: 'callback', 32 | component: Callback 33 | }, 34 | { 35 | path: '/unauthorized', 36 | name: 'Unauthorized', 37 | component: Unauthorized 38 | }, 39 | ] 40 | }) 41 | 42 | export default router; 43 | 44 | router.beforeEach(async (to, from, next) => { 45 | let app = router.app.$data || {isAuthenticated: false} 46 | if (app.isAuthenticated) { 47 | //already signed in, we can navigate anywhere 48 | next() 49 | } else if (to.matched.some(record => record.meta.requiresAuth)) { 50 | //authentication is required. Trigger the sign in process, including the return URI 51 | let authenticate = router.app.authenticate 52 | authenticate(to.path).then(() => { 53 | console.log('authenticating a protected url:' + to.path); 54 | next(); 55 | }); 56 | } else { 57 | //No auth required. We can navigate 58 | next() 59 | } 60 | }); -------------------------------------------------------------------------------- /vue-app/src/services/security.js: -------------------------------------------------------------------------------- 1 | import Oidc from 'oidc-client'; 2 | 3 | var mgr = new Oidc.UserManager({ 4 | authority: 'https://localhost:5443', 5 | client_id: 'js', 6 | redirect_uri: 'https://localhost:5000/callback', 7 | response_type: 'id_token token', 8 | scope: 'openid profile api1', 9 | post_logout_redirect_uri: 'https://localhost:5000/', 10 | userStore: new Oidc.WebStorageStateStore({ store: window.localStorage }), 11 | 12 | automaticSilentRenew: true, 13 | silent_redirect_uri: 'https://localhost:5000/static/silent-renew.html', 14 | accessTokenExpiringNotificationTime: 10, 15 | // filterProtocolClaims: true, 16 | // loadUserInfo: true 17 | }) 18 | 19 | // Oidc.Log.logger = console; 20 | // Oidc.Log.level = Oidc.Log.INFO; 21 | 22 | // mgr.events.addUserLoaded(async function (user) { 23 | // await store.dispatch('refreshUserInfo'); 24 | // await store.dispatch('ensureUserIsKnown'); 25 | // }); 26 | 27 | // mgr.events.addAccessTokenExpiring(function () { 28 | // // console.log('AccessToken Expiring:', arguments); 29 | // }); 30 | 31 | // mgr.events.addAccessTokenExpired(function () { 32 | // mgr.signoutRedirect().then(function (resp) { 33 | // store.commit('set_user', null); //clear user details in vuex 34 | // }).catch(function (err) { 35 | // console.log(err) 36 | // }) 37 | // }); 38 | 39 | // mgr.events.addSilentRenewError(function () { 40 | // console.error('Silent Renew Error:', arguments); 41 | // }); 42 | 43 | // mgr.events.addUserSignedOut(function () { 44 | // mgr.signoutRedirect().then(function (resp) { 45 | // store.commit('set_user', null); //clear user details in vuex 46 | // }).catch(function (err) { 47 | // console.log(err) 48 | // }) 49 | // }); 50 | 51 | // class SecurityService { 52 | 53 | // constructor(){ 54 | // // console.log('Creating SecurityService instance') 55 | // } 56 | 57 | // async getUser () { 58 | // let self = this; 59 | // return new Promise((resolve, reject) => { 60 | // mgr.getUser().then(function (user) { 61 | // if (user == null) { 62 | // return resolve(null) 63 | // } else { 64 | // return resolve(user) 65 | // } 66 | // }).catch(function (err) { 67 | // console.log(err) 68 | // return reject(err) 69 | // }); 70 | // }) 71 | // } 72 | 73 | // signIn (returnToUrl) { 74 | // returnToUrl ? mgr.signinRedirect({ state: returnToUrl }) 75 | // : mgr.signinRedirect(); 76 | // } 77 | 78 | // signOut () { 79 | // var self = this; 80 | // mgr.signoutRedirect().then(function (resp) { 81 | // // console.log('signed out', resp); 82 | // }).catch(function (err) { 83 | // console.log(err) 84 | // }) 85 | // } 86 | // } 87 | 88 | // let service = new SecurityService(); 89 | 90 | // export {service as default, mgr}; 91 | 92 | export default mgr; -------------------------------------------------------------------------------- /vue-app/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 48 | 49 | -------------------------------------------------------------------------------- /vue-app/src/views/Callback.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /vue-app/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /vue-app/src/views/Unauthorized.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-app/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | outputDir: '../vueApi/wwwroot' 3 | } --------------------------------------------------------------------------------