├── .gitignore ├── IdentityServerAndApi.sln ├── IdentityServerHost ├── Config.cs ├── IdentityServerHost.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Quickstart │ ├── Account │ │ ├── AccountController.cs │ │ ├── AccountOptions.cs │ │ ├── ExternalController.cs │ │ ├── ExternalProvider.cs │ │ ├── LoggedOutViewModel.cs │ │ ├── LoginInputModel.cs │ │ ├── LoginViewModel.cs │ │ ├── LogoutInputModel.cs │ │ ├── LogoutViewModel.cs │ │ └── RedirectViewModel.cs │ ├── Consent │ │ ├── ConsentController.cs │ │ ├── ConsentInputModel.cs │ │ ├── ConsentOptions.cs │ │ ├── ConsentViewModel.cs │ │ ├── ProcessConsentResult.cs │ │ └── ScopeViewModel.cs │ ├── Device │ │ ├── DeviceAuthorizationInputModel.cs │ │ ├── DeviceAuthorizationViewModel.cs │ │ └── DeviceController.cs │ ├── Diagnostics │ │ ├── DiagnosticsController.cs │ │ └── DiagnosticsViewModel.cs │ ├── Extensions.cs │ ├── Grants │ │ ├── GrantsController.cs │ │ └── GrantsViewModel.cs │ ├── Home │ │ ├── ErrorViewModel.cs │ │ └── HomeController.cs │ ├── SecurityHeadersAttribute.cs │ └── TestUsers.cs ├── Startup.cs ├── TestController.cs ├── Views │ ├── Account │ │ ├── LoggedOut.cshtml │ │ ├── Login.cshtml │ │ └── Logout.cshtml │ ├── Consent │ │ └── Index.cshtml │ ├── Device │ │ ├── Success.cshtml │ │ ├── UserCodeCapture.cshtml │ │ └── UserCodeConfirmation.cshtml │ ├── Diagnostics │ │ └── Index.cshtml │ ├── Grants │ │ └── Index.cshtml │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── Redirect.cshtml │ │ ├── _Layout.cshtml │ │ ├── _ScopeListItem.cshtml │ │ └── _ValidationSummary.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── tempkey.rsa └── wwwroot │ ├── app.js │ ├── callback.html │ ├── css │ ├── site.css │ ├── site.less │ └── site.min.css │ ├── favicon.ico │ ├── icon.jpg │ ├── icon.png │ ├── index.html │ ├── js │ ├── signin-redirect.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 │ ├── oidc-client.js │ └── oidc-client.min.js │ ├── popup.html │ └── silent.html ├── LICENSE ├── README.md ├── RunAndTest.bat ├── SecureAPITests ├── SecureAPITests.csproj └── TestApiEndpoints.cs └── azure-pipelines.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | # StyleCop 57 | StyleCopReport.xml 58 | 59 | # Files built by Visual Studio 60 | *_i.c 61 | *_p.c 62 | *_i.h 63 | *.ilk 64 | *.meta 65 | *.obj 66 | *.iobj 67 | *.pch 68 | *.pdb 69 | *.ipdb 70 | *.pgc 71 | *.pgd 72 | *.rsp 73 | *.sbr 74 | *.tlb 75 | *.tli 76 | *.tlh 77 | *.tmp 78 | *.tmp_proj 79 | *.log 80 | *.vspscc 81 | *.vssscc 82 | .builds 83 | *.pidb 84 | *.svclog 85 | *.scc 86 | 87 | # Chutzpah Test files 88 | _Chutzpah* 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opendb 95 | *.opensdf 96 | *.sdf 97 | *.cachefile 98 | *.VC.db 99 | *.VC.VC.opendb 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | *.sap 106 | 107 | # Visual Studio Trace Files 108 | *.e2e 109 | 110 | # TFS 2012 Local Workspace 111 | $tf/ 112 | 113 | # Guidance Automation Toolkit 114 | *.gpState 115 | 116 | # ReSharper is a .NET coding add-in 117 | _ReSharper*/ 118 | *.[Rr]e[Ss]harper 119 | *.DotSettings.user 120 | 121 | # JustCode is a .NET coding add-in 122 | .JustCode 123 | 124 | # TeamCity is a build add-in 125 | _TeamCity* 126 | 127 | # DotCover is a Code Coverage Tool 128 | *.dotCover 129 | 130 | # AxoCover is a Code Coverage Tool 131 | .axoCover/* 132 | !.axoCover/settings.json 133 | 134 | # Visual Studio code coverage results 135 | *.coverage 136 | *.coveragexml 137 | 138 | # NCrunch 139 | _NCrunch_* 140 | .*crunch*.local.xml 141 | nCrunchTemp_* 142 | 143 | # MightyMoose 144 | *.mm.* 145 | AutoTest.Net/ 146 | 147 | # Web workbench (sass) 148 | .sass-cache/ 149 | 150 | # Installshield output folder 151 | [Ee]xpress/ 152 | 153 | # DocProject is a documentation generator add-in 154 | DocProject/buildhelp/ 155 | DocProject/Help/*.HxT 156 | DocProject/Help/*.HxC 157 | DocProject/Help/*.hhc 158 | DocProject/Help/*.hhk 159 | DocProject/Help/*.hhp 160 | DocProject/Help/Html2 161 | DocProject/Help/html 162 | 163 | # Click-Once directory 164 | publish/ 165 | 166 | # Publish Web Output 167 | *.[Pp]ublish.xml 168 | *.azurePubxml 169 | # Note: Comment the next line if you want to checkin your web deploy settings, 170 | # but database connection strings (with potential passwords) will be unencrypted 171 | *.pubxml 172 | *.publishproj 173 | 174 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 175 | # checkin your Azure Web App publish settings, but sensitive information contained 176 | # in these scripts will be unencrypted 177 | PublishScripts/ 178 | 179 | # NuGet Packages 180 | *.nupkg 181 | # The packages folder can be ignored because of Package Restore 182 | **/[Pp]ackages/* 183 | # except build/, which is used as an MSBuild target. 184 | !**/[Pp]ackages/build/ 185 | # Uncomment if necessary however generally it will be regenerated when needed 186 | #!**/[Pp]ackages/repositories.config 187 | # NuGet v3's project.json files produces more ignorable files 188 | *.nuget.props 189 | *.nuget.targets 190 | 191 | # Microsoft Azure Build Output 192 | csx/ 193 | *.build.csdef 194 | 195 | # Microsoft Azure Emulator 196 | ecf/ 197 | rcf/ 198 | 199 | # Windows Store app package directories and files 200 | AppPackages/ 201 | BundleArtifacts/ 202 | Package.StoreAssociation.xml 203 | _pkginfo.txt 204 | *.appx 205 | 206 | # Visual Studio cache files 207 | # files ending in .cache can be ignored 208 | *.[Cc]ache 209 | # but keep track of directories ending in .cache 210 | !*.[Cc]ache/ 211 | 212 | # Others 213 | ClientBin/ 214 | ~$* 215 | *~ 216 | *.dbmdl 217 | *.dbproj.schemaview 218 | *.jfm 219 | *.pfx 220 | *.publishsettings 221 | orleans.codegen.cs 222 | 223 | # Including strong name files can present a security risk 224 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 225 | #*.snk 226 | 227 | # Since there are multiple workflows, uncomment next line to ignore bower_components 228 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 229 | #bower_components/ 230 | 231 | # RIA/Silverlight projects 232 | Generated_Code/ 233 | 234 | # Backup & report files from converting an old project file 235 | # to a newer Visual Studio version. Backup files are not needed, 236 | # because we have git ;-) 237 | _UpgradeReport_Files/ 238 | Backup*/ 239 | UpgradeLog*.XML 240 | UpgradeLog*.htm 241 | ServiceFabricBackup/ 242 | *.rptproj.bak 243 | 244 | # SQL Server files 245 | *.mdf 246 | *.ldf 247 | *.ndf 248 | 249 | # Business Intelligence projects 250 | *.rdl.data 251 | *.bim.layout 252 | *.bim_*.settings 253 | *.rptproj.rsuser 254 | 255 | # Microsoft Fakes 256 | FakesAssemblies/ 257 | 258 | # GhostDoc plugin setting file 259 | *.GhostDoc.xml 260 | 261 | # Node.js Tools for Visual Studio 262 | .ntvs_analysis.dat 263 | node_modules/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ 330 | -------------------------------------------------------------------------------- /IdentityServerAndApi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28803.452 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityServerHost", "IdentityServerHost\IdentityServerHost.csproj", "{BFC294D2-BF94-41AD-95B8-712814D15E89}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecureAPITests", "SecureAPITests\SecureAPITests.csproj", "{06585C71-3424-4800-A997-DF83ED1357C1}" 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 | {BFC294D2-BF94-41AD-95B8-712814D15E89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {BFC294D2-BF94-41AD-95B8-712814D15E89}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {BFC294D2-BF94-41AD-95B8-712814D15E89}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {BFC294D2-BF94-41AD-95B8-712814D15E89}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {06585C71-3424-4800-A997-DF83ED1357C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {06585C71-3424-4800-A997-DF83ED1357C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {06585C71-3424-4800-A997-DF83ED1357C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {06585C71-3424-4800-A997-DF83ED1357C1}.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 = {87CD59D1-07BD-483B-BF66-13E2C3DF050F} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /IdentityServerHost/Config.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using IdentityServer4; 3 | using IdentityServer4.Models; 4 | 5 | namespace IdentityServerHost 6 | { 7 | public class Config 8 | { 9 | public const string BASE_URL = "http://localhost:3611"; 10 | 11 | public static IEnumerable Clients = new List 12 | { 13 | new Client 14 | { 15 | ClientId = "spa", 16 | AllowedGrantTypes = GrantTypes.CodeAndClientCredentials, 17 | RequireClientSecret = false, 18 | RequirePkce = true, 19 | RequireConsent = false, 20 | RedirectUris = { 21 | $"{BASE_URL}/callback.html", 22 | $"{BASE_URL}/popup.html", 23 | $"{BASE_URL}/silent.html" 24 | }, 25 | PostLogoutRedirectUris = { $"{BASE_URL}/index.html" }, 26 | AllowedScopes = { "openid", "profile", "email", IdentityServerConstants.LocalApi.ScopeName }, 27 | AllowedCorsOrigins = { BASE_URL } 28 | }, 29 | }; 30 | 31 | public static IEnumerable IdentityResources = new List 32 | { 33 | new IdentityResources.OpenId(), 34 | new IdentityResources.Profile(), 35 | new IdentityResources.Email(), 36 | }; 37 | 38 | public static IEnumerable Apis = new List 39 | { 40 | // local API 41 | new ApiResource(IdentityServerConstants.LocalApi.ScopeName), 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /IdentityServerHost/IdentityServerHost.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /IdentityServerHost/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace IdentityServerHost 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | BuildWebHost(args).Run(); 12 | } 13 | 14 | public static IWebHost BuildWebHost(string[] args) => 15 | WebHost.CreateDefaultBuilder(args) 16 | .ConfigureLogging(builder => 17 | { 18 | builder.SetMinimumLevel(LogLevel.Warning); 19 | builder.AddFilter("IdentityServer4", LogLevel.Debug); 20 | }) 21 | .UseStartup() 22 | .Build(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /IdentityServerHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54325/", 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 | "IdentityServerHost": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:3611/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /IdentityServerHost/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.Events; 7 | using IdentityServer4.Extensions; 8 | using IdentityServer4.Models; 9 | using IdentityServer4.Services; 10 | using IdentityServer4.Stores; 11 | using IdentityServer4.Test; 12 | using Microsoft.AspNetCore.Authentication; 13 | using Microsoft.AspNetCore.Authorization; 14 | using Microsoft.AspNetCore.Http; 15 | using Microsoft.AspNetCore.Mvc; 16 | using System; 17 | using System.Linq; 18 | using System.Threading.Tasks; 19 | 20 | namespace IdentityServerHost 21 | { 22 | /// 23 | /// This sample controller implements a typical login/logout/provision workflow for local and external accounts. 24 | /// The login service encapsulates the interactions with the user data store. This data store is in-memory only and cannot be used for production! 25 | /// The interaction service provides a way for the UI to communicate with identityserver for validation and context retrieval 26 | /// 27 | [SecurityHeaders] 28 | [AllowAnonymous] 29 | public class AccountController : Controller 30 | { 31 | private readonly TestUserStore _users; 32 | private readonly IIdentityServerInteractionService _interaction; 33 | private readonly IClientStore _clientStore; 34 | private readonly IAuthenticationSchemeProvider _schemeProvider; 35 | private readonly IEventService _events; 36 | 37 | public AccountController( 38 | IIdentityServerInteractionService interaction, 39 | IClientStore clientStore, 40 | IAuthenticationSchemeProvider schemeProvider, 41 | IEventService events, 42 | TestUserStore users = null) 43 | { 44 | // if the TestUserStore is not in DI, then we'll just use the global users collection 45 | // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) 46 | _users = users ?? new TestUserStore(TestUsers.Users); 47 | 48 | _interaction = interaction; 49 | _clientStore = clientStore; 50 | _schemeProvider = schemeProvider; 51 | _events = events; 52 | } 53 | 54 | /// 55 | /// Entry point into the login workflow 56 | /// 57 | [HttpGet] 58 | public async Task Login(string returnUrl) 59 | { 60 | // build a model so we know what to show on the login page 61 | var vm = await BuildLoginViewModelAsync(returnUrl); 62 | 63 | if (vm.IsExternalLoginOnly) 64 | { 65 | // we only have one option for logging in and it's an external provider 66 | return RedirectToAction("Challenge", "External", new { provider = vm.ExternalLoginScheme, returnUrl }); 67 | } 68 | 69 | return View(vm); 70 | } 71 | 72 | /// 73 | /// Handle postback from username/password login 74 | /// 75 | [HttpPost] 76 | [ValidateAntiForgeryToken] 77 | public async Task Login(LoginInputModel model, string button) 78 | { 79 | // check if we are in the context of an authorization request 80 | var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); 81 | 82 | // the user clicked the "cancel" button 83 | if (button != "login") 84 | { 85 | if (context != null) 86 | { 87 | // if the user cancels, send a result back into IdentityServer as if they 88 | // denied the consent (even if this client does not require consent). 89 | // this will send back an access denied OIDC error response to the client. 90 | await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); 91 | 92 | // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null 93 | if (await _clientStore.IsPkceClientAsync(context.ClientId)) 94 | { 95 | // if the client is PKCE then we assume it's native, so this change in how to 96 | // return the response is for better UX for the end user. 97 | return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl }); 98 | } 99 | 100 | return Redirect(model.ReturnUrl); 101 | } 102 | else 103 | { 104 | // since we don't have a valid context, then we just go back to the home page 105 | return Redirect("~/"); 106 | } 107 | } 108 | 109 | if (ModelState.IsValid) 110 | { 111 | // validate username/password against in-memory store 112 | if (_users.ValidateCredentials(model.Username, model.Password)) 113 | { 114 | var user = _users.FindByUsername(model.Username); 115 | await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); 116 | 117 | // only set explicit expiration here if user chooses "remember me". 118 | // otherwise we rely upon expiration configured in cookie middleware. 119 | AuthenticationProperties props = null; 120 | if (AccountOptions.AllowRememberLogin && model.RememberLogin) 121 | { 122 | props = new AuthenticationProperties 123 | { 124 | IsPersistent = true, 125 | ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) 126 | }; 127 | }; 128 | 129 | // issue authentication cookie with subject ID and username 130 | await HttpContext.SignInAsync(user.SubjectId, user.Username, props); 131 | 132 | if (context != null) 133 | { 134 | if (await _clientStore.IsPkceClientAsync(context.ClientId)) 135 | { 136 | // if the client is PKCE then we assume it's native, so this change in how to 137 | // return the response is for better UX for the end user. 138 | return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl }); 139 | } 140 | 141 | // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null 142 | return Redirect(model.ReturnUrl); 143 | } 144 | 145 | // request for a local page 146 | if (Url.IsLocalUrl(model.ReturnUrl)) 147 | { 148 | return Redirect(model.ReturnUrl); 149 | } 150 | else if (string.IsNullOrEmpty(model.ReturnUrl)) 151 | { 152 | return Redirect("~/"); 153 | } 154 | else 155 | { 156 | // user might have clicked on a malicious link - should be logged 157 | throw new Exception("invalid return URL"); 158 | } 159 | } 160 | 161 | await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); 162 | ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage); 163 | } 164 | 165 | // something went wrong, show form with error 166 | var vm = await BuildLoginViewModelAsync(model); 167 | return View(vm); 168 | } 169 | 170 | 171 | /// 172 | /// Show logout page 173 | /// 174 | [HttpGet] 175 | public async Task Logout(string logoutId) 176 | { 177 | // build a model so the logout page knows what to display 178 | var vm = await BuildLogoutViewModelAsync(logoutId); 179 | 180 | if (vm.ShowLogoutPrompt == false) 181 | { 182 | // if the request for logout was properly authenticated from IdentityServer, then 183 | // we don't need to show the prompt and can just log the user out directly. 184 | return await Logout(vm); 185 | } 186 | 187 | return View(vm); 188 | } 189 | 190 | /// 191 | /// Handle logout page postback 192 | /// 193 | [HttpPost] 194 | [ValidateAntiForgeryToken] 195 | public async Task Logout(LogoutInputModel model) 196 | { 197 | // build a model so the logged out page knows what to display 198 | var vm = await BuildLoggedOutViewModelAsync(model.LogoutId); 199 | 200 | if (User?.Identity.IsAuthenticated == true) 201 | { 202 | // delete local authentication cookie 203 | await HttpContext.SignOutAsync(); 204 | 205 | // raise the logout event 206 | await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName())); 207 | } 208 | 209 | // check if we need to trigger sign-out at an upstream identity provider 210 | if (vm.TriggerExternalSignout) 211 | { 212 | // build a return URL so the upstream provider will redirect back 213 | // to us after the user has logged out. this allows us to then 214 | // complete our single sign-out processing. 215 | string url = Url.Action("Logout", new { logoutId = vm.LogoutId }); 216 | 217 | // this triggers a redirect to the external provider for sign-out 218 | return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme); 219 | } 220 | 221 | return View("LoggedOut", vm); 222 | } 223 | 224 | 225 | 226 | /*****************************************/ 227 | /* helper APIs for the AccountController */ 228 | /*****************************************/ 229 | private async Task BuildLoginViewModelAsync(string returnUrl) 230 | { 231 | var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 232 | if (context?.IdP != null) 233 | { 234 | var local = context.IdP == IdentityServer4.IdentityServerConstants.LocalIdentityProvider; 235 | 236 | // this is meant to short circuit the UI and only trigger the one external IdP 237 | var vm = new LoginViewModel 238 | { 239 | EnableLocalLogin = local, 240 | ReturnUrl = returnUrl, 241 | Username = context?.LoginHint, 242 | }; 243 | 244 | if (!local) 245 | { 246 | vm.ExternalProviders = new[] { new ExternalProvider { AuthenticationScheme = context.IdP } }; 247 | } 248 | 249 | return vm; 250 | } 251 | 252 | var schemes = await _schemeProvider.GetAllSchemesAsync(); 253 | 254 | var providers = schemes 255 | .Where(x => x.DisplayName != null || 256 | (x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) 257 | ) 258 | .Select(x => new ExternalProvider 259 | { 260 | DisplayName = x.DisplayName, 261 | AuthenticationScheme = x.Name 262 | }).ToList(); 263 | 264 | var allowLocal = true; 265 | if (context?.ClientId != null) 266 | { 267 | var client = await _clientStore.FindEnabledClientByIdAsync(context.ClientId); 268 | if (client != null) 269 | { 270 | allowLocal = client.EnableLocalLogin; 271 | 272 | if (client.IdentityProviderRestrictions != null && client.IdentityProviderRestrictions.Any()) 273 | { 274 | providers = providers.Where(provider => client.IdentityProviderRestrictions.Contains(provider.AuthenticationScheme)).ToList(); 275 | } 276 | } 277 | } 278 | 279 | return new LoginViewModel 280 | { 281 | AllowRememberLogin = AccountOptions.AllowRememberLogin, 282 | EnableLocalLogin = allowLocal && AccountOptions.AllowLocalLogin, 283 | ReturnUrl = returnUrl, 284 | Username = context?.LoginHint, 285 | ExternalProviders = providers.ToArray() 286 | }; 287 | } 288 | 289 | private async Task BuildLoginViewModelAsync(LoginInputModel model) 290 | { 291 | var vm = await BuildLoginViewModelAsync(model.ReturnUrl); 292 | vm.Username = model.Username; 293 | vm.RememberLogin = model.RememberLogin; 294 | return vm; 295 | } 296 | 297 | private async Task BuildLogoutViewModelAsync(string logoutId) 298 | { 299 | var vm = new LogoutViewModel { LogoutId = logoutId, ShowLogoutPrompt = AccountOptions.ShowLogoutPrompt }; 300 | 301 | if (User?.Identity.IsAuthenticated != true) 302 | { 303 | // if the user is not authenticated, then just show logged out page 304 | vm.ShowLogoutPrompt = false; 305 | return vm; 306 | } 307 | 308 | var context = await _interaction.GetLogoutContextAsync(logoutId); 309 | if (context?.ShowSignoutPrompt == false) 310 | { 311 | // it's safe to automatically sign-out 312 | vm.ShowLogoutPrompt = false; 313 | return vm; 314 | } 315 | 316 | // show the logout prompt. this prevents attacks where the user 317 | // is automatically signed out by another malicious web page. 318 | return vm; 319 | } 320 | 321 | private async Task BuildLoggedOutViewModelAsync(string logoutId) 322 | { 323 | // get context information (client name, post logout redirect URI and iframe for federated signout) 324 | var logout = await _interaction.GetLogoutContextAsync(logoutId); 325 | 326 | var vm = new LoggedOutViewModel 327 | { 328 | AutomaticRedirectAfterSignOut = AccountOptions.AutomaticRedirectAfterSignOut, 329 | PostLogoutRedirectUri = logout?.PostLogoutRedirectUri, 330 | ClientName = string.IsNullOrEmpty(logout?.ClientName) ? logout?.ClientId : logout?.ClientName, 331 | SignOutIframeUrl = logout?.SignOutIFrameUrl, 332 | LogoutId = logoutId 333 | }; 334 | 335 | if (User?.Identity.IsAuthenticated == true) 336 | { 337 | var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value; 338 | if (idp != null && idp != IdentityServer4.IdentityServerConstants.LocalIdentityProvider) 339 | { 340 | var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp); 341 | if (providerSupportsSignout) 342 | { 343 | if (vm.LogoutId == null) 344 | { 345 | // if there's no current logout context, we need to create one 346 | // this captures necessary info from the current logged in user 347 | // before we signout and redirect away to the external IdP for signout 348 | vm.LogoutId = await _interaction.CreateLogoutContextAsync(); 349 | } 350 | 351 | vm.ExternalAuthenticationScheme = idp; 352 | } 353 | } 354 | } 355 | 356 | return vm; 357 | } 358 | } 359 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/AccountOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | 7 | namespace IdentityServerHost 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 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/ExternalController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Security.Principal; 6 | using System.Threading.Tasks; 7 | using IdentityModel; 8 | using IdentityServer4.Events; 9 | using IdentityServerHost; 10 | using IdentityServer4.Services; 11 | using IdentityServer4.Stores; 12 | using IdentityServer4.Test; 13 | using Microsoft.AspNetCore.Authentication; 14 | using Microsoft.AspNetCore.Authorization; 15 | using Microsoft.AspNetCore.Http; 16 | using Microsoft.AspNetCore.Mvc; 17 | 18 | namespace IdentityServerHost 19 | { 20 | [SecurityHeaders] 21 | [AllowAnonymous] 22 | public class ExternalController : Controller 23 | { 24 | private readonly TestUserStore _users; 25 | private readonly IIdentityServerInteractionService _interaction; 26 | private readonly IClientStore _clientStore; 27 | private readonly IEventService _events; 28 | 29 | public ExternalController( 30 | IIdentityServerInteractionService interaction, 31 | IClientStore clientStore, 32 | IEventService events, 33 | TestUserStore users = null) 34 | { 35 | // if the TestUserStore is not in DI, then we'll just use the global users collection 36 | // this is where you would plug in your own custom identity management library (e.g. ASP.NET Identity) 37 | _users = users ?? new TestUserStore(TestUsers.Users); 38 | 39 | _interaction = interaction; 40 | _clientStore = clientStore; 41 | _events = events; 42 | } 43 | 44 | /// 45 | /// initiate roundtrip to external authentication provider 46 | /// 47 | [HttpGet] 48 | public async Task Challenge(string provider, string returnUrl) 49 | { 50 | if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; 51 | 52 | // validate returnUrl - either it is a valid OIDC URL or back to a local page 53 | if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false) 54 | { 55 | // user might have clicked on a malicious link - should be logged 56 | throw new Exception("invalid return URL"); 57 | } 58 | 59 | if (AccountOptions.WindowsAuthenticationSchemeName == provider) 60 | { 61 | // windows authentication needs special handling 62 | return await ProcessWindowsLoginAsync(returnUrl); 63 | } 64 | else 65 | { 66 | // start challenge and roundtrip the return URL and scheme 67 | var props = new AuthenticationProperties 68 | { 69 | RedirectUri = Url.Action(nameof(Callback)), 70 | Items = 71 | { 72 | { "returnUrl", returnUrl }, 73 | { "scheme", provider }, 74 | } 75 | }; 76 | 77 | return Challenge(props, provider); 78 | } 79 | } 80 | 81 | /// 82 | /// Post processing of external authentication 83 | /// 84 | [HttpGet] 85 | public async Task Callback() 86 | { 87 | // read external identity from the temporary cookie 88 | var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); 89 | if (result?.Succeeded != true) 90 | { 91 | throw new Exception("External authentication error"); 92 | } 93 | 94 | // lookup our user and external provider info 95 | var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result); 96 | if (user == null) 97 | { 98 | // this might be where you might initiate a custom workflow for user registration 99 | // in this sample we don't show how that would be done, as our sample implementation 100 | // simply auto-provisions new external user 101 | user = AutoProvisionUser(provider, providerUserId, claims); 102 | } 103 | 104 | // this allows us to collect any additonal claims or properties 105 | // for the specific prtotocols used and store them in the local auth cookie. 106 | // this is typically used to store data needed for signout from those protocols. 107 | var additionalLocalClaims = new List(); 108 | var localSignInProps = new AuthenticationProperties(); 109 | ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps); 110 | ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps); 111 | ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps); 112 | 113 | // issue authentication cookie for user 114 | await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username)); 115 | await HttpContext.SignInAsync(user.SubjectId, user.Username, provider, localSignInProps, additionalLocalClaims.ToArray()); 116 | 117 | // delete temporary cookie used during external authentication 118 | await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); 119 | 120 | // retrieve return URL 121 | var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; 122 | 123 | // check if external login is in the context of an OIDC request 124 | var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 125 | if (context != null) 126 | { 127 | if (await _clientStore.IsPkceClientAsync(context.ClientId)) 128 | { 129 | // if the client is PKCE then we assume it's native, so this change in how to 130 | // return the response is for better UX for the end user. 131 | return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl }); 132 | } 133 | } 134 | 135 | return Redirect(returnUrl); 136 | } 137 | 138 | private async Task ProcessWindowsLoginAsync(string returnUrl) 139 | { 140 | // see if windows auth has already been requested and succeeded 141 | var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName); 142 | if (result?.Principal is WindowsPrincipal wp) 143 | { 144 | // we will issue the external cookie and then redirect the 145 | // user back to the external callback, in essence, treating windows 146 | // auth the same as any other external authentication mechanism 147 | var props = new AuthenticationProperties() 148 | { 149 | RedirectUri = Url.Action("Callback"), 150 | Items = 151 | { 152 | { "returnUrl", returnUrl }, 153 | { "scheme", AccountOptions.WindowsAuthenticationSchemeName }, 154 | } 155 | }; 156 | 157 | var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName); 158 | id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.Identity.Name)); 159 | id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); 160 | 161 | // add the groups as claims -- be careful if the number of groups is too large 162 | if (AccountOptions.IncludeWindowsGroups) 163 | { 164 | var wi = wp.Identity as WindowsIdentity; 165 | var groups = wi.Groups.Translate(typeof(NTAccount)); 166 | var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value)); 167 | id.AddClaims(roles); 168 | } 169 | 170 | await HttpContext.SignInAsync( 171 | IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme, 172 | new ClaimsPrincipal(id), 173 | props); 174 | return Redirect(props.RedirectUri); 175 | } 176 | else 177 | { 178 | // trigger windows auth 179 | // since windows auth don't support the redirect uri, 180 | // this URL is re-triggered when we call challenge 181 | return Challenge(AccountOptions.WindowsAuthenticationSchemeName); 182 | } 183 | } 184 | 185 | private (TestUser user, string provider, string providerUserId, IEnumerable claims) FindUserFromExternalProvider(AuthenticateResult result) 186 | { 187 | var externalUser = result.Principal; 188 | 189 | // try to determine the unique id of the external user (issued by the provider) 190 | // the most common claim type for that are the sub claim and the NameIdentifier 191 | // depending on the external provider, some other claim type might be used 192 | var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ?? 193 | externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? 194 | throw new Exception("Unknown userid"); 195 | 196 | // remove the user id claim so we don't include it as an extra claim if/when we provision the user 197 | var claims = externalUser.Claims.ToList(); 198 | claims.Remove(userIdClaim); 199 | 200 | var provider = result.Properties.Items["scheme"]; 201 | var providerUserId = userIdClaim.Value; 202 | 203 | // find external user 204 | var user = _users.FindByExternalProvider(provider, providerUserId); 205 | 206 | return (user, provider, providerUserId, claims); 207 | } 208 | 209 | private TestUser AutoProvisionUser(string provider, string providerUserId, IEnumerable claims) 210 | { 211 | var user = _users.AutoProvisionUser(provider, providerUserId, claims.ToList()); 212 | return user; 213 | } 214 | 215 | private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 216 | { 217 | // if the external system sent a session id claim, copy it over 218 | // so we can use it for single sign-out 219 | var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); 220 | if (sid != null) 221 | { 222 | localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); 223 | } 224 | 225 | // if the external provider issued an id_token, we'll keep it for signout 226 | var id_token = externalResult.Properties.GetTokenValue("id_token"); 227 | if (id_token != null) 228 | { 229 | localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); 230 | } 231 | } 232 | 233 | private void ProcessLoginCallbackForWsFed(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 234 | { 235 | } 236 | 237 | private void ProcessLoginCallbackForSaml2p(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 238 | { 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/ExternalProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost 6 | { 7 | public class ExternalProvider 8 | { 9 | public string DisplayName { get; set; } 10 | public string AuthenticationScheme { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost 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; } = false; 14 | 15 | public string LogoutId { get; set; } 16 | public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; 17 | public string ExternalAuthenticationScheme { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/LoginInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServerHost 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 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace IdentityServerHost 10 | { 11 | public class LoginViewModel : LoginInputModel 12 | { 13 | public bool AllowRememberLogin { get; set; } = true; 14 | public bool EnableLocalLogin { get; set; } = true; 15 | 16 | public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); 17 | public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); 18 | 19 | public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; 20 | public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; 21 | } 22 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/LogoutInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost 6 | { 7 | public class LogoutInputModel 8 | { 9 | public string LogoutId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/LogoutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost 6 | { 7 | public class LogoutViewModel : LogoutInputModel 8 | { 9 | public bool ShowLogoutPrompt { get; set; } = true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Account/RedirectViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | 6 | namespace IdentityServerHost 7 | { 8 | public class RedirectViewModel 9 | { 10 | public string RedirectUrl { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityServerHost/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.Events; 6 | using IdentityServer4.Models; 7 | using IdentityServer4.Services; 8 | using IdentityServer4.Stores; 9 | using IdentityServer4.Extensions; 10 | using Microsoft.AspNetCore.Authorization; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.Extensions.Logging; 13 | using System.Linq; 14 | using System.Threading.Tasks; 15 | 16 | namespace IdentityServerHost 17 | { 18 | /// 19 | /// This controller processes the consent UI 20 | /// 21 | [SecurityHeaders] 22 | [Authorize] 23 | public class ConsentController : Controller 24 | { 25 | private readonly IIdentityServerInteractionService _interaction; 26 | private readonly IClientStore _clientStore; 27 | private readonly IResourceStore _resourceStore; 28 | private readonly IEventService _events; 29 | private readonly ILogger _logger; 30 | 31 | public ConsentController( 32 | IIdentityServerInteractionService interaction, 33 | IClientStore clientStore, 34 | IResourceStore resourceStore, 35 | IEventService events, 36 | ILogger logger) 37 | { 38 | _interaction = interaction; 39 | _clientStore = clientStore; 40 | _resourceStore = resourceStore; 41 | _events = events; 42 | _logger = logger; 43 | } 44 | 45 | /// 46 | /// Shows the consent screen 47 | /// 48 | /// 49 | /// 50 | [HttpGet] 51 | public async Task Index(string returnUrl) 52 | { 53 | var vm = await BuildViewModelAsync(returnUrl); 54 | if (vm != null) 55 | { 56 | return View("Index", vm); 57 | } 58 | 59 | return View("Error"); 60 | } 61 | 62 | /// 63 | /// Handles the consent screen postback 64 | /// 65 | [HttpPost] 66 | [ValidateAntiForgeryToken] 67 | public async Task Index(ConsentInputModel model) 68 | { 69 | var result = await ProcessConsent(model); 70 | 71 | if (result.IsRedirect) 72 | { 73 | if (await _clientStore.IsPkceClientAsync(result.ClientId)) 74 | { 75 | // if the client is PKCE then we assume it's native, so this change in how to 76 | // return the response is for better UX for the end user. 77 | return View("Redirect", new RedirectViewModel { RedirectUrl = result.RedirectUri }); 78 | } 79 | 80 | return Redirect(result.RedirectUri); 81 | } 82 | 83 | if (result.HasValidationError) 84 | { 85 | ModelState.AddModelError(string.Empty, result.ValidationError); 86 | } 87 | 88 | if (result.ShowView) 89 | { 90 | return View("Index", result.ViewModel); 91 | } 92 | 93 | return View("Error"); 94 | } 95 | 96 | /*****************************************/ 97 | /* helper APIs for the ConsentController */ 98 | /*****************************************/ 99 | private async Task ProcessConsent(ConsentInputModel model) 100 | { 101 | var result = new ProcessConsentResult(); 102 | 103 | // validate return url is still valid 104 | var request = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); 105 | if (request == null) return result; 106 | 107 | ConsentResponse grantedConsent = null; 108 | 109 | // user clicked 'no' - send back the standard 'access_denied' response 110 | if (model?.Button == "no") 111 | { 112 | grantedConsent = ConsentResponse.Denied; 113 | 114 | // emit event 115 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested)); 116 | } 117 | // user clicked 'yes' - validate the data 118 | else if (model?.Button == "yes") 119 | { 120 | // if the user consented to some scope, build the response model 121 | if (model.ScopesConsented != null && model.ScopesConsented.Any()) 122 | { 123 | var scopes = model.ScopesConsented; 124 | if (ConsentOptions.EnableOfflineAccess == false) 125 | { 126 | scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); 127 | } 128 | 129 | grantedConsent = new ConsentResponse 130 | { 131 | RememberConsent = model.RememberConsent, 132 | ScopesConsented = scopes.ToArray() 133 | }; 134 | 135 | // emit event 136 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent)); 137 | } 138 | else 139 | { 140 | result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; 141 | } 142 | } 143 | else 144 | { 145 | result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; 146 | } 147 | 148 | if (grantedConsent != null) 149 | { 150 | // communicate outcome of consent back to identityserver 151 | await _interaction.GrantConsentAsync(request, grantedConsent); 152 | 153 | // indicate that's it ok to redirect back to authorization endpoint 154 | result.RedirectUri = model.ReturnUrl; 155 | result.ClientId = request.ClientId; 156 | } 157 | else 158 | { 159 | // we need to redisplay the consent UI 160 | result.ViewModel = await BuildViewModelAsync(model.ReturnUrl, model); 161 | } 162 | 163 | return result; 164 | } 165 | 166 | private async Task BuildViewModelAsync(string returnUrl, ConsentInputModel model = null) 167 | { 168 | var request = await _interaction.GetAuthorizationContextAsync(returnUrl); 169 | if (request != null) 170 | { 171 | var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); 172 | if (client != null) 173 | { 174 | var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); 175 | if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) 176 | { 177 | return CreateConsentViewModel(model, returnUrl, request, client, resources); 178 | } 179 | else 180 | { 181 | _logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y)); 182 | } 183 | } 184 | else 185 | { 186 | _logger.LogError("Invalid client id: {0}", request.ClientId); 187 | } 188 | } 189 | else 190 | { 191 | _logger.LogError("No consent request matching request: {0}", returnUrl); 192 | } 193 | 194 | return null; 195 | } 196 | 197 | private ConsentViewModel CreateConsentViewModel( 198 | ConsentInputModel model, string returnUrl, 199 | AuthorizationRequest request, 200 | Client client, Resources resources) 201 | { 202 | var vm = new ConsentViewModel 203 | { 204 | RememberConsent = model?.RememberConsent ?? true, 205 | ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), 206 | 207 | ReturnUrl = returnUrl, 208 | 209 | ClientName = client.ClientName ?? client.ClientId, 210 | ClientUrl = client.ClientUri, 211 | ClientLogoUrl = client.LogoUri, 212 | AllowRememberConsent = client.AllowRememberConsent 213 | }; 214 | 215 | vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 216 | vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 217 | if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) 218 | { 219 | vm.ResourceScopes = vm.ResourceScopes.Union(new ScopeViewModel[] { 220 | GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) 221 | }); 222 | } 223 | 224 | return vm; 225 | } 226 | 227 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 228 | { 229 | return new ScopeViewModel 230 | { 231 | Name = identity.Name, 232 | DisplayName = identity.DisplayName, 233 | Description = identity.Description, 234 | Emphasize = identity.Emphasize, 235 | Required = identity.Required, 236 | Checked = check || identity.Required 237 | }; 238 | } 239 | 240 | public ScopeViewModel CreateScopeViewModel(Scope scope, bool check) 241 | { 242 | return new ScopeViewModel 243 | { 244 | Name = scope.Name, 245 | DisplayName = scope.DisplayName, 246 | Description = scope.Description, 247 | Emphasize = scope.Emphasize, 248 | Required = scope.Required, 249 | Checked = check || scope.Required 250 | }; 251 | } 252 | 253 | private ScopeViewModel GetOfflineAccessScope(bool check) 254 | { 255 | return new ScopeViewModel 256 | { 257 | Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, 258 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 259 | Description = ConsentOptions.OfflineAccessDescription, 260 | Emphasize = true, 261 | Checked = check 262 | }; 263 | } 264 | } 265 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Consent/ConsentInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServerHost 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 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost 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 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Consent/ConsentViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServerHost 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 | -------------------------------------------------------------------------------- /IdentityServerHost/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 IdentityServerHost 6 | { 7 | public class ProcessConsentResult 8 | { 9 | public bool IsRedirect => RedirectUri != null; 10 | public string RedirectUri { get; set; } 11 | public string ClientId { get; set; } 12 | 13 | public bool ShowView => ViewModel != null; 14 | public ConsentViewModel ViewModel { get; set; } 15 | 16 | public bool HasValidationError => ValidationError != null; 17 | public string ValidationError { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Consent/ScopeViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost 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 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Device/DeviceAuthorizationInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Device 6 | { 7 | public class DeviceAuthorizationInputModel : ConsentInputModel 8 | { 9 | public string UserCode { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Device/DeviceAuthorizationViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServerHost.Device 6 | { 7 | public class DeviceAuthorizationViewModel : ConsentViewModel 8 | { 9 | public string UserCode { get; set; } 10 | public bool ConfirmUserCode { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Device/DeviceController.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.Linq; 7 | using System.Threading.Tasks; 8 | using IdentityServer4.Events; 9 | using IdentityServer4.Extensions; 10 | using IdentityServer4.Models; 11 | using IdentityServer4.Services; 12 | using IdentityServer4.Stores; 13 | using Microsoft.AspNetCore.Authorization; 14 | using Microsoft.AspNetCore.Mvc; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace IdentityServerHost.Device 18 | { 19 | [Authorize] 20 | [SecurityHeaders] 21 | public class DeviceController : Controller 22 | { 23 | private readonly IDeviceFlowInteractionService _interaction; 24 | private readonly IClientStore _clientStore; 25 | private readonly IResourceStore _resourceStore; 26 | private readonly IEventService _events; 27 | private readonly ILogger _logger; 28 | 29 | public DeviceController( 30 | IDeviceFlowInteractionService interaction, 31 | IClientStore clientStore, 32 | IResourceStore resourceStore, 33 | IEventService eventService, 34 | ILogger logger) 35 | { 36 | _interaction = interaction; 37 | _clientStore = clientStore; 38 | _resourceStore = resourceStore; 39 | _events = eventService; 40 | _logger = logger; 41 | } 42 | 43 | [HttpGet] 44 | public async Task Index([FromQuery(Name = "user_code")] string userCode) 45 | { 46 | if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture"); 47 | 48 | var vm = await BuildViewModelAsync(userCode); 49 | if (vm == null) return View("Error"); 50 | 51 | vm.ConfirmUserCode = true; 52 | return View("UserCodeConfirmation", vm); 53 | } 54 | 55 | [HttpPost] 56 | [ValidateAntiForgeryToken] 57 | public async Task UserCodeCapture(string userCode) 58 | { 59 | var vm = await BuildViewModelAsync(userCode); 60 | if (vm == null) return View("Error"); 61 | 62 | return View("UserCodeConfirmation", vm); 63 | } 64 | 65 | [HttpPost] 66 | [ValidateAntiForgeryToken] 67 | public async Task Callback(DeviceAuthorizationInputModel model) 68 | { 69 | if (model == null) throw new ArgumentNullException(nameof(model)); 70 | 71 | var result = await ProcessConsent(model); 72 | if (result.HasValidationError) return View("Error"); 73 | 74 | return View("Success"); 75 | } 76 | 77 | private async Task ProcessConsent(DeviceAuthorizationInputModel model) 78 | { 79 | var result = new ProcessConsentResult(); 80 | 81 | var request = await _interaction.GetAuthorizationContextAsync(model.UserCode); 82 | if (request == null) return result; 83 | 84 | ConsentResponse grantedConsent = null; 85 | 86 | // user clicked 'no' - send back the standard 'access_denied' response 87 | if (model.Button == "no") 88 | { 89 | grantedConsent = ConsentResponse.Denied; 90 | 91 | // emit event 92 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested)); 93 | } 94 | // user clicked 'yes' - validate the data 95 | else if (model.Button == "yes") 96 | { 97 | // if the user consented to some scope, build the response model 98 | if (model.ScopesConsented != null && model.ScopesConsented.Any()) 99 | { 100 | var scopes = model.ScopesConsented; 101 | if (ConsentOptions.EnableOfflineAccess == false) 102 | { 103 | scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); 104 | } 105 | 106 | grantedConsent = new ConsentResponse 107 | { 108 | RememberConsent = model.RememberConsent, 109 | ScopesConsented = scopes.ToArray() 110 | }; 111 | 112 | // emit event 113 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent)); 114 | } 115 | else 116 | { 117 | result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; 118 | } 119 | } 120 | else 121 | { 122 | result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; 123 | } 124 | 125 | if (grantedConsent != null) 126 | { 127 | // communicate outcome of consent back to identityserver 128 | await _interaction.HandleRequestAsync(model.UserCode, grantedConsent); 129 | 130 | // indicate that's it ok to redirect back to authorization endpoint 131 | result.RedirectUri = model.ReturnUrl; 132 | result.ClientId = request.ClientId; 133 | } 134 | else 135 | { 136 | // we need to redisplay the consent UI 137 | result.ViewModel = await BuildViewModelAsync(model.UserCode, model); 138 | } 139 | 140 | return result; 141 | } 142 | 143 | private async Task BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null) 144 | { 145 | var request = await _interaction.GetAuthorizationContextAsync(userCode); 146 | if (request != null) 147 | { 148 | var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); 149 | if (client != null) 150 | { 151 | var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); 152 | if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) 153 | { 154 | return CreateConsentViewModel(userCode, model, client, resources); 155 | } 156 | else 157 | { 158 | _logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y)); 159 | } 160 | } 161 | else 162 | { 163 | _logger.LogError("Invalid client id: {0}", request.ClientId); 164 | } 165 | } 166 | 167 | return null; 168 | } 169 | 170 | private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, Client client, Resources resources) 171 | { 172 | var vm = new DeviceAuthorizationViewModel 173 | { 174 | UserCode = userCode, 175 | 176 | RememberConsent = model?.RememberConsent ?? true, 177 | ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), 178 | 179 | ClientName = client.ClientName ?? client.ClientId, 180 | ClientUrl = client.ClientUri, 181 | ClientLogoUrl = client.LogoUri, 182 | AllowRememberConsent = client.AllowRememberConsent 183 | }; 184 | 185 | vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 186 | vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 187 | if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) 188 | { 189 | vm.ResourceScopes = vm.ResourceScopes.Union(new[] 190 | { 191 | GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) 192 | }); 193 | } 194 | 195 | return vm; 196 | } 197 | 198 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 199 | { 200 | return new ScopeViewModel 201 | { 202 | Name = identity.Name, 203 | DisplayName = identity.DisplayName, 204 | Description = identity.Description, 205 | Emphasize = identity.Emphasize, 206 | Required = identity.Required, 207 | Checked = check || identity.Required 208 | }; 209 | } 210 | 211 | public ScopeViewModel CreateScopeViewModel(Scope scope, bool check) 212 | { 213 | return new ScopeViewModel 214 | { 215 | Name = scope.Name, 216 | DisplayName = scope.DisplayName, 217 | Description = scope.Description, 218 | Emphasize = scope.Emphasize, 219 | Required = scope.Required, 220 | Checked = check || scope.Required 221 | }; 222 | } 223 | private ScopeViewModel GetOfflineAccessScope(bool check) 224 | { 225 | return new ScopeViewModel 226 | { 227 | Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, 228 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 229 | Description = ConsentOptions.OfflineAccessDescription, 230 | Emphasize = true, 231 | Checked = check 232 | }; 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Diagnostics/DiagnosticsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace IdentityServerHost 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 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Diagnostics/DiagnosticsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Newtonsoft.Json; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | 11 | namespace IdentityServerHost 12 | { 13 | public class DiagnosticsViewModel 14 | { 15 | public DiagnosticsViewModel(AuthenticateResult result) 16 | { 17 | AuthenticateResult = result; 18 | 19 | if (result.Properties.Items.ContainsKey("client_list")) 20 | { 21 | var encoded = result.Properties.Items["client_list"]; 22 | var bytes = Base64Url.Decode(encoded); 23 | var value = Encoding.UTF8.GetString(bytes); 24 | 25 | Clients = JsonConvert.DeserializeObject(value); 26 | } 27 | } 28 | 29 | public AuthenticateResult AuthenticateResult { get; } 30 | public IEnumerable Clients { get; } = new List(); 31 | } 32 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using IdentityServer4.Stores; 3 | 4 | namespace IdentityServerHost 5 | { 6 | public static class Extensions 7 | { 8 | /// 9 | /// Determines whether the client is configured to use PKCE. 10 | /// 11 | /// The store. 12 | /// The client identifier. 13 | /// 14 | public static async Task IsPkceClientAsync(this IClientStore store, string client_id) 15 | { 16 | if (!string.IsNullOrWhiteSpace(client_id)) 17 | { 18 | var client = await store.FindEnabledClientByIdAsync(client_id); 19 | return client?.RequirePkce == true; 20 | } 21 | 22 | return false; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Grants/GrantsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using IdentityServer4.Stores; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.Authorization; 12 | using IdentityServer4.Events; 13 | using IdentityServer4.Extensions; 14 | 15 | namespace IdentityServerHost 16 | { 17 | /// 18 | /// This sample controller allows a user to revoke grants given to clients 19 | /// 20 | [SecurityHeaders] 21 | [Authorize] 22 | public class GrantsController : Controller 23 | { 24 | private readonly IIdentityServerInteractionService _interaction; 25 | private readonly IClientStore _clients; 26 | private readonly IResourceStore _resources; 27 | private readonly IEventService _events; 28 | 29 | public GrantsController(IIdentityServerInteractionService interaction, 30 | IClientStore clients, 31 | IResourceStore resources, 32 | IEventService events) 33 | { 34 | _interaction = interaction; 35 | _clients = clients; 36 | _resources = resources; 37 | _events = events; 38 | } 39 | 40 | /// 41 | /// Show list of grants 42 | /// 43 | [HttpGet] 44 | public async Task Index() 45 | { 46 | return View("Index", await BuildViewModelAsync()); 47 | } 48 | 49 | /// 50 | /// Handle postback to revoke a client 51 | /// 52 | [HttpPost] 53 | [ValidateAntiForgeryToken] 54 | public async Task Revoke(string clientId) 55 | { 56 | await _interaction.RevokeUserConsentAsync(clientId); 57 | await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId)); 58 | 59 | return RedirectToAction("Index"); 60 | } 61 | 62 | private async Task BuildViewModelAsync() 63 | { 64 | var grants = await _interaction.GetAllUserConsentsAsync(); 65 | 66 | var list = new List(); 67 | foreach(var grant in grants) 68 | { 69 | var client = await _clients.FindClientByIdAsync(grant.ClientId); 70 | if (client != null) 71 | { 72 | var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); 73 | 74 | var item = new GrantViewModel() 75 | { 76 | ClientId = client.ClientId, 77 | ClientName = client.ClientName ?? client.ClientId, 78 | ClientLogoUrl = client.LogoUri, 79 | ClientUrl = client.ClientUri, 80 | Created = grant.CreationTime, 81 | Expires = grant.Expiration, 82 | IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), 83 | ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray() 84 | }; 85 | 86 | list.Add(item); 87 | } 88 | } 89 | 90 | return new GrantsViewModel 91 | { 92 | Grants = list 93 | }; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Grants/GrantsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace IdentityServerHost 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 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Home/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServerHost 8 | { 9 | public class ErrorViewModel 10 | { 11 | public ErrorMessage Error { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | using System.Threading.Tasks; 11 | 12 | namespace IdentityServerHost 13 | { 14 | [SecurityHeaders] 15 | [AllowAnonymous] 16 | public class HomeController : Controller 17 | { 18 | private readonly IIdentityServerInteractionService _interaction; 19 | private readonly IHostingEnvironment _environment; 20 | private readonly ILogger _logger; 21 | 22 | public HomeController(IIdentityServerInteractionService interaction, IHostingEnvironment environment, ILogger logger) 23 | { 24 | _interaction = interaction; 25 | _environment = environment; 26 | _logger = logger; 27 | } 28 | 29 | public IActionResult Index() 30 | { 31 | if (_environment.IsDevelopment()) 32 | { 33 | // only show in development 34 | return View(); 35 | } 36 | 37 | _logger.LogInformation("Homepage is disabled in production. Returning 404."); 38 | return NotFound(); 39 | } 40 | 41 | /// 42 | /// Shows the error page 43 | /// 44 | public async Task Error(string errorId) 45 | { 46 | var vm = new ErrorViewModel(); 47 | 48 | // retrieve error details from identityserver 49 | var message = await _interaction.GetErrorContextAsync(errorId); 50 | if (message != null) 51 | { 52 | vm.Error = message; 53 | 54 | if (!_environment.IsDevelopment()) 55 | { 56 | // only show in development 57 | message.ErrorDescription = null; 58 | } 59 | } 60 | 61 | return View("Error", vm); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /IdentityServerHost/Quickstart/SecurityHeadersAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | 8 | namespace IdentityServerHost 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 | -------------------------------------------------------------------------------- /IdentityServerHost/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 IdentityServerHost 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 | } -------------------------------------------------------------------------------- /IdentityServerHost/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Diagnostics.HealthChecks; 10 | 11 | namespace IdentityServerHost 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddMvc(); 25 | 26 | services.AddIdentityServer() 27 | .AddDeveloperSigningCredential() 28 | .AddInMemoryIdentityResources(Config.IdentityResources) 29 | .AddInMemoryClients(Config.Clients) 30 | .AddInMemoryApiResources(Config.Apis) 31 | .AddTestUsers(TestUsers.Users); 32 | 33 | services.AddHealthChecks() 34 | .AddCheck("Simple", () => HealthCheckResult.Healthy("Healthy!")); 35 | 36 | services.AddLocalApiAuthentication(); 37 | } 38 | 39 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 40 | { 41 | if (env.IsDevelopment()) 42 | { 43 | app.UseDeveloperExceptionPage(); 44 | } 45 | else 46 | { 47 | app.UseExceptionHandler("/Home/Error"); 48 | } 49 | 50 | app.UseDefaultFiles(); 51 | app.UseStaticFiles(); 52 | 53 | app.UseIdentityServer(); 54 | app.UseHealthChecks("/health"); 55 | app.UseMvc(routes => 56 | { 57 | routes.MapRoute( 58 | name: "default", 59 | template: "{controller=Home}/{action=Index}/{id?}"); 60 | }); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /IdentityServerHost/TestController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using IdentityServer4; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace WebApi 7 | { 8 | public class TestController : ControllerBase 9 | { 10 | [Route("test")] 11 | [Authorize(IdentityServerConstants.LocalApi.PolicyName)] 12 | public IActionResult Get() 13 | { 14 | var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToArray(); 15 | return Ok(new { message = "Hello API", claims }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /IdentityServerHost/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 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model LoginViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityServerHost/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 |
-------------------------------------------------------------------------------- /IdentityServerHost/Views/Consent/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model ConsentViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/Device/Success.cshtml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/Device/UserCodeCapture.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/Device/UserCodeConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @model IdentityServerHost.Device.DeviceAuthorizationViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityServerHost/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 | } -------------------------------------------------------------------------------- /IdentityServerHost/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 |
-------------------------------------------------------------------------------- /IdentityServerHost/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | var version = typeof(IdentityServer4.Hosting.IdentityServerMiddleware).Assembly.GetName().Version.ToString(); 3 | } 4 | 5 |
6 | 15 | 16 |
17 |
18 |

19 | IdentityServer publishes a 20 | discovery document 21 | where you can find metadata and links to all the endpoints, key material, etc. 22 |

23 |
24 |
25 |

26 | Click here to manage your stored grants. 27 |

28 |
29 |
30 |
31 |
32 |

33 | Here are links to the 34 | source code repository, 35 | and ready to use samples. 36 |

37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | 3 | @{ 4 | var error = Model?.Error?.Error; 5 | var errorDescription = Model?.Error?.ErrorDescription; 6 | var request_id = Model?.Error?.RequestId; 7 | } 8 | 9 |
10 | 13 | 14 |
15 |
16 |
17 | Sorry, there was an error 18 | 19 | @if (error != null) 20 | { 21 | 22 | 23 | : @error 24 | 25 | 26 | 27 | if (errorDescription != null) 28 | { 29 |
@errorDescription
30 | } 31 | } 32 |
33 | 34 | @if (request_id != null) 35 | { 36 |
Request Id: @request_id
37 | } 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/Shared/Redirect.cshtml: -------------------------------------------------------------------------------- 1 | @model RedirectViewModel 2 | 3 |

You are now being returned to the application.

4 |

Once complete, you may close this tab

5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IdentityServerHost/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 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/Shared/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @model ScopeViewModel 2 | 3 |
  • 4 | 24 | @if (Model.Required) 25 | { 26 | (required) 27 | } 28 | @if (Model.Description != null) 29 | { 30 | 33 | } 34 |
  • -------------------------------------------------------------------------------- /IdentityServerHost/Views/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /IdentityServerHost/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServerHost 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /IdentityServerHost/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /IdentityServerHost/tempkey.rsa: -------------------------------------------------------------------------------- 1 | {"KeyId":"2a2467e4a208110eb83896572a7c10af","Parameters":{"D":"o1jfRilLZlEfBwW2IbALkLOSvVa0DbnnSdTtY9a60FfuxVP7KocIY0LyTRsZMBwOQt1XJKk3GCiMGzVEoffN9LFBgN9TOIYYCnWzFDZDfClutJFD1SEeNv69rtz864ExoQuyNKsjbpLSHiwgwXYr7YzIqLG3YvGM4jxcmV/WNuhT/FEakbuc0CqHg0pezRNrrhhp/jzg8ptuo2Zrs11Z2/9mZMKjpNMWeCoMTJfc23tCPAAR8Jlwk+QnR9z9BByAUPAX+vMKbjuM6dg/lSaCRmTdtctV38CAb3jcg0oCcl0zhuSOPQhfkajoVYlDHcKOkrtXgr6g8g9xrncMNl76rQ==","DP":"Lv8w5/RLdwurG/gdT0wtiGZZH/1By3csUkOsU3qpg7DS8Jg0bOi12olBabEldDLsuVM/GxTGXmpRuem1H3ZWORlJIMRD2AJoLfzKNgZw3yJgSGn6TBamlAZFvQ5kUu3KdOdnGmk4G7cpYjMfJU6wdXtFu0p94a7oKZtyfG4Afdc=","DQ":"K6dP/ba01bT0w53JvchgoM2Nm2aC5BjjfqZR+fM5PlmUNUMOwzrrAG1dwtVQJKMxI/0KdIlEx2IN8IqzSOKJBFT7MVveX8rq9Ckay0IXXb9nQuYTXeooBQ3peaMkWJqQhfbJMVWP0b1+ELhA6gG33AgR8T8+KMOiFRi9kbc5o4k=","Exponent":"AQAB","InverseQ":"WEj8Ac0HXFVBMvRhgievtXAqha5iJXgmueWZyhTvW24qTWJdGOPvd50ZXSKAmNiLg7XquDEb3APuF6kbPSgOUWKayDUIaaDuRF+hnus6O/i13Ap6MMpKAw7FUZ5pCBWWOG6eeD5DTL/nokKqKT/xeJkJ4cNxtGu9tr2EoAbUTpQ=","Modulus":"whSXRxmfI0n2bj+w4BCyAAFaRQBwJnTI52hch3J+ufIU7/WIKx30FTwWL1Fy0o4hsui0yl+YyaVlqawPT+rpDUsNduoIsl+7BE92mCbV5v0TUi4pmtsMhuTpspIOKGDbOF0F9y3GomKNjLvscLzAeb/XjgZglb7nGW884F9rcOLh3J6x2Q9STA+XEaHYSAMbHY4fO0oNn3kEB6oTt5sYum3FtRmvTmLypzqTB0FQU+ANsoQmV/EfY5VdPeUItFhoMk8TqYHSIB7KnInp8f7Sgp9UikYqiOYZ+QtqgC0epb7ol09Tz8wj8sBkRl0wr1+XM9c6kfKCCbPBGLPNlM7pUQ==","P":"xD4s+LqFtMrgud3do7yumgF+K6d+TqrFnTA1WwPItjLVy00/tlu+vnbn5D9327sNbY9dH9pxemy7H2wC9H3ypgEtlwNSMfsTwkuWi/2yz0yzZoS3oSavIAg/3NX05cHxdkSrS1huFNVdZvqn2LAfH8DEGWIpdxlUDv/rgu+Oq0s=","Q":"/S3YbCuOGcbI02HmWE+1iZwZ8FNUyc34A9jd17OW6sSzw0Swq08SG+tMNyqRZddaJdZ2BbPo0PErGmJ/2svv2BUDULh+xNrRT2JMZoK7lCKMgyga6exhIE3mQPz2wEUT244psAugG9mSrCOPUhH8JJvR0FxrHMRYpHKaeKHsIFM="}} -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/app.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | var config = { 4 | authority: "http://localhost:3611/", 5 | client_id: "spa", 6 | redirect_uri: window.location.origin + "/callback.html", 7 | post_logout_redirect_uri: window.location.origin + "/index.html", 8 | 9 | // these two will be done dynamically from the buttons clicked, but are 10 | // needed if you want to use the silent_renew 11 | response_type: "code", 12 | scope: "openid profile email IdentityServerApi", 13 | 14 | // this will toggle if profile endpoint is used 15 | loadUserInfo: true, 16 | 17 | // silent renew will get a new access_token via an iframe 18 | // just prior to the old access_token expiring (60 seconds prior) 19 | silent_redirect_uri: window.location.origin + "/silent.html", 20 | automaticSilentRenew: true 21 | }; 22 | Oidc.Log.logger = window.console; 23 | Oidc.Log.level = Oidc.Log.INFO; 24 | 25 | var mgr = new Oidc.UserManager(config); 26 | 27 | mgr.events.addUserLoaded(function (user) { 28 | log("User loaded"); 29 | showTokens(); 30 | }); 31 | mgr.events.addUserUnloaded(function () { 32 | log("User logged out locally"); 33 | showTokens(); 34 | }); 35 | mgr.events.addAccessTokenExpiring(function () { 36 | log("Access token expiring..."); 37 | }); 38 | mgr.events.addSilentRenewError(function (err) { 39 | log("Silent renew error: " + err.message); 40 | }); 41 | mgr.events.addUserSignedOut(function () { 42 | log("User signed out of OP"); 43 | }); 44 | 45 | function login() { 46 | mgr.signinRedirect(); 47 | } 48 | 49 | function logout() { 50 | mgr.signoutRedirect(); 51 | } 52 | 53 | function callApi() { 54 | mgr.getUser().then(function (user) { 55 | var xhr = new XMLHttpRequest(); 56 | xhr.onload = function (e) { 57 | if (xhr.status >= 400) { 58 | display("#ajax-result", { 59 | status: xhr.status, 60 | statusText: xhr.statusText, 61 | wwwAuthenticate: xhr.getResponseHeader("WWW-Authenticate") 62 | }); 63 | } 64 | else { 65 | display("#ajax-result", xhr.response); 66 | } 67 | }; 68 | xhr.open("GET", "http://localhost:3611/test", true); 69 | xhr.setRequestHeader("Authorization", "Bearer " + user.access_token); 70 | xhr.send(); 71 | }); 72 | } 73 | 74 | if (window.location.hash) { 75 | handleCallback(); 76 | } 77 | 78 | document.querySelector(".login").addEventListener("click", login, false); 79 | document.querySelector(".call").addEventListener("click", callApi, false); 80 | document.querySelector(".logout").addEventListener("click", logout, false); 81 | 82 | 83 | function log(data) { 84 | document.getElementById('response').innerText = ''; 85 | 86 | Array.prototype.forEach.call(arguments, function (msg) { 87 | if (msg instanceof Error) { 88 | msg = "Error: " + msg.message; 89 | } 90 | else if (typeof msg !== 'string') { 91 | msg = JSON.stringify(msg, null, 2); 92 | } 93 | document.getElementById('response').innerHTML += msg + '\r\n'; 94 | }); 95 | } 96 | 97 | function display(selector, data) { 98 | if (data && typeof data === 'string') { 99 | try { 100 | data = JSON.parse(data); 101 | } 102 | catch (e) { } 103 | } 104 | if (data && typeof data !== 'string') { 105 | data = JSON.stringify(data, null, 2); 106 | } 107 | document.querySelector(selector).textContent = data; 108 | } 109 | 110 | function showTokens() { 111 | mgr.getUser().then(function (user) { 112 | if (user) { 113 | display("#id-token", user); 114 | } 115 | else { 116 | log("Not logged in"); 117 | } 118 | }); 119 | } 120 | showTokens(); 121 | 122 | function handleCallback() { 123 | mgr.signinRedirectCallback().then(function (user) { 124 | var hash = window.location.hash.substr(1); 125 | var result = hash.split('&').reduce(function (result, item) { 126 | var parts = item.split('='); 127 | result[parts[0]] = parts[1]; 128 | return result; 129 | }, {}); 130 | 131 | log(result); 132 | showTokens(); 133 | 134 | window.history.replaceState({}, 135 | window.document.title, 136 | window.location.origin + window.location.pathname); 137 | 138 | }, function (error) { 139 | log(error); 140 | }); 141 | } -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/callback.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 |
    9 | 12 | 13 |
    14 |
    15 | Back to index 16 |
    17 |
    18 |
    19 | 20 | 21 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /IdentityServerHost/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 | } -------------------------------------------------------------------------------- /IdentityServerHost/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 | -------------------------------------------------------------------------------- /IdentityServerHost/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:'';} -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/TestSecureApiSample/68812ca083d93aa5f47066183ead9f52302be3fb/IdentityServerHost/wwwroot/favicon.ico -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/TestSecureApiSample/68812ca083d93aa5f47066183ead9f52302be3fb/IdentityServerHost/wwwroot/icon.jpg -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/TestSecureApiSample/68812ca083d93aa5f47066183ead9f52302be3fb/IdentityServerHost/wwwroot/icon.png -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 |
    14 | 17 | 18 |
    19 |
      20 |
    • Home
    • 21 |
    • 22 |
    • 23 |
    • 24 |
    25 |
    26 | 27 |
    28 |
      29 |
    30 |
    31 | 32 |
    33 |
    34 |
    Message
    35 |
    36 |
    
    37 |                 
    38 |
    39 |
    40 | 41 |
    42 |
    43 |
    44 |
    Current User
    45 |
    46 |
    
    47 |                     
    48 |
    49 |
    50 | 51 | 59 | 60 |
    61 |
    62 |
    Ajax Result
    63 |
    64 |
    
    65 |                     
    66 |
    67 |
    68 |
    69 |
    70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/js/signin-redirect.js: -------------------------------------------------------------------------------- 1 | window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); 2 | -------------------------------------------------------------------------------- /IdentityServerHost/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 | -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/TestSecureApiSample/68812ca083d93aa5f47066183ead9f52302be3fb/IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/TestSecureApiSample/68812ca083d93aa5f47066183ead9f52302be3fb/IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/TestSecureApiSample/68812ca083d93aa5f47066183ead9f52302be3fb/IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/TestSecureApiSample/68812ca083d93aa5f47066183ead9f52302be3fb/IdentityServerHost/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /IdentityServerHost/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); -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/popup.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /IdentityServerHost/wwwroot/silent.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Steve Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TestSecureApiSample 2 | 3 | [![Build Status](https://dev.azure.com/ardalis/TestSecureApiSample/_apis/build/status/ardalis.TestSecureApiSample?branchName=master)](https://dev.azure.com/ardalis/TestSecureApiSample/_build/latest?definitionId=1&branchName=master) 4 | 5 | A sample showing how to test a secure API endpoint using xunit, identityserver4, and environment variables. 6 | 7 | ## Goals 8 | 9 | - Demonstrate how to test a token-secured API endpoint running live (perhaps in a container) using xUnit 10 | - Demonstrate how to dynamically specify the API endpoint's URL using environment variables read by xUnit 11 | 12 | ## Original Sample 13 | 14 | Original **IdentityServerHost** project forked from here: 15 | [https://github.com/brockallen/IdentityServerAndApi](https://github.com/brockallen/IdentityServerAndApi) and only modified slightly. 16 | 17 | ## Expected Output using Environment Variables for Configuration 18 | 19 | ```console 20 | Microsoft Windows [Version 10.0.17763.557] 21 | (c) 2018 Microsoft Corporation. All rights reserved. 22 | 23 | C:\dev\Scratch\IdentityServerAndApi\SecureAPITests>SET ApiBaseUrl=http://google.com 24 | 25 | C:\dev\Scratch\IdentityServerAndApi\SecureAPITests>dotnet test 26 | Test run for C:\dev\Scratch\IdentityServerAndApi\SecureAPITests\bin\Debug\netcoreapp3.0\SecureAPITests.dll(.NETCoreApp,Version=v3.0) 27 | Microsoft (R) Test Execution Command Line Tool Version 16.0.1 28 | Copyright (c) Microsoft Corporation. All rights reserved. 29 | 30 | Starting test execution, please wait... 31 | [xUnit.net 00:00:00.61] SecureAPITests.UnitTest1.HitApiEndpoint [FAIL] 32 | Failed SecureAPITests.UnitTest1.HitApiEndpoint 33 | Error Message: 34 | Assert.True() Failure 35 | Expected: True 36 | Actual: False 37 | Stack Trace: 38 | at SecureAPITests.UnitTest1.HitApiEndpoint() in C:\dev\Scratch\IdentityServerAndApi\SecureAPITests\UnitTest1.cs:line 92 39 | --- End of stack trace from previous location where exception was thrown --- 40 | 41 | Total tests: 2. Passed: 1. Failed: 1. Skipped: 0. 42 | Test Run Failed. 43 | Test execution time: 1.0766 Seconds 44 | 45 | C:\dev\Scratch\IdentityServerAndApi\SecureAPITests>SET ApiBaseUrl=http://localhost:5000 46 | 47 | C:\dev\Scratch\IdentityServerAndApi\SecureAPITests>dotnet test 48 | Test run for C:\dev\Scratch\IdentityServerAndApi\SecureAPITests\bin\Debug\netcoreapp3.0\SecureAPITests.dll(.NETCoreApp,Version=v3.0) 49 | Microsoft (R) Test Execution Command Line Tool Version 16.0.1 50 | Copyright (c) Microsoft Corporation. All rights reserved. 51 | 52 | Starting test execution, please wait... 53 | 54 | Total tests: 2. Passed: 2. Failed: 0. Skipped: 0. 55 | Test Run Successful. 56 | Test execution time: 0.9730 Seconds 57 | 58 | C:\dev\Scratch\IdentityServerAndApi\SecureAPITests> 59 | ``` 60 | 61 | ## Notes 62 | 63 | The app should be running on port 3611 using the checked-in launchSettings.json file. If not, you should adjust it to do so. 64 | 65 | If ctrl-F5 in Visual Studio to run the API (IdentityServerHost project) and then when you run the tests it just dies with an "exited with -1" try running both the web server and the tests from two separate command lines, instead. 66 | -------------------------------------------------------------------------------- /RunAndTest.bat: -------------------------------------------------------------------------------- 1 | pushd IdentityServerHost 2 | start dotnet run 3 | popd 4 | pushd SecureApiTests 5 | dotnet test 6 | popd 7 | -------------------------------------------------------------------------------- /SecureAPITests/SecureAPITests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /SecureAPITests/TestApiEndpoints.cs: -------------------------------------------------------------------------------- 1 | using IdentityModel.Client; 2 | using IdentityServer4; 3 | using IdentityServerHost; 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace SecureAPITests 11 | { 12 | public class TestApiEndpoints 13 | { 14 | public string IdentityBaseUrl { get; set; } = Config.BASE_URL; 15 | public string ApiBaseUrl { get; set; } = Config.BASE_URL; 16 | 17 | public TestApiEndpoints() 18 | { 19 | string identityBaseUrl = Environment.GetEnvironmentVariable("IdentityBaseUrl"); 20 | if (!String.IsNullOrEmpty(identityBaseUrl)) 21 | { 22 | IdentityBaseUrl = identityBaseUrl; 23 | } 24 | string apiBaseUrl = Environment.GetEnvironmentVariable("ApiBaseUrl"); 25 | if (!String.IsNullOrEmpty(apiBaseUrl)) 26 | { 27 | ApiBaseUrl = apiBaseUrl; 28 | } 29 | } 30 | 31 | private async Task GetAccessToken() 32 | { 33 | var client = new HttpClient(); 34 | var disco = await client.GetDiscoveryDocumentAsync(IdentityBaseUrl); 35 | if (!String.IsNullOrEmpty(disco.Error)) 36 | { 37 | throw new Exception(disco.Error); 38 | } 39 | var response = await client.RequestTokenAsync(new TokenRequest 40 | { 41 | Address = disco.TokenEndpoint, 42 | GrantType = IdentityModel.OidcConstants.GrantTypes.ClientCredentials, 43 | ClientId = "spa", 44 | 45 | Parameters = 46 | { 47 | { "username", "alice"}, 48 | { "password", "alice"}, 49 | { "scope", IdentityServerConstants.LocalApi.ScopeName } 50 | } 51 | }); 52 | return response.AccessToken; 53 | } 54 | 55 | [Fact] 56 | public async Task GetAccessTokenWithAliceCreds() 57 | { 58 | string token = await GetAccessToken(); 59 | 60 | Assert.False(string.IsNullOrWhiteSpace(token)); 61 | } 62 | 63 | [Fact] 64 | public async Task HitApiEndpoint() 65 | { 66 | string token = await GetAccessToken(); 67 | 68 | var apiClient = new HttpClient(); 69 | apiClient.SetBearerToken(token); 70 | 71 | var apiResponse = await apiClient.GetAsync($"{ApiBaseUrl}/test"); 72 | 73 | Assert.True(apiResponse.IsSuccessStatusCode); 74 | 75 | var stringResponse = await apiResponse.Content.ReadAsStringAsync(); 76 | 77 | dynamic result = JsonConvert.DeserializeAnonymousType(stringResponse, new { message = "" }); 78 | Assert.Equal("Hello API", result.message); 79 | } 80 | 81 | [Fact] 82 | public async Task GetPublicHealthEndpoint() 83 | { 84 | var apiClient = new HttpClient(); 85 | 86 | var apiResponse = await apiClient.GetAsync($"{ApiBaseUrl}/health"); 87 | 88 | Assert.True(apiResponse.IsSuccessStatusCode); 89 | 90 | var stringResponse = await apiResponse.Content.ReadAsStringAsync(); 91 | 92 | Assert.Equal("Healthy", stringResponse); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - master 5 | - refs/tags/* 6 | pr: 7 | - master 8 | 9 | pool: 10 | vmImage: 'VS2017-Win2016' 11 | 12 | variables: 13 | BuildConfiguration: 'Release' 14 | 15 | steps: 16 | - task: DotNetCoreCLI@2 17 | displayName: Restore 18 | inputs: 19 | command: restore 20 | projects: '**/*.csproj' 21 | 22 | - task: DotNetCoreCLI@2 23 | displayName: Build 24 | inputs: 25 | projects: '**/*.csproj' 26 | arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)' 27 | 28 | - task: DotNetCoreCLI@2 29 | displayName: Test 30 | inputs: 31 | command: test 32 | projects: '**/*[Tt]ests/*.csproj' 33 | arguments: '--configuration $(BuildConfiguration)' 34 | --------------------------------------------------------------------------------