├── .gitattributes ├── .gitignore ├── BlazorClient ├── App.razor ├── BlazorClient.csproj ├── Pages │ ├── Authentication.razor │ ├── Claims.razor │ ├── Counter.razor │ ├── FetchData.razor │ └── Index.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Security │ └── ArrayClaimsPrincipalFactory.cs ├── Shared │ ├── LoginDisplay.razor │ ├── MainLayout.razor │ ├── NavMenu.razor │ ├── RedirectToLogin.razor │ └── SurveyPrompt.razor ├── _Imports.razor └── wwwroot │ ├── appsettings.json │ ├── css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ ├── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ │ ├── css │ │ │ └── open-iconic-bootstrap.min.css │ │ │ └── fonts │ │ │ ├── open-iconic.eot │ │ │ ├── open-iconic.otf │ │ │ ├── open-iconic.svg │ │ │ ├── open-iconic.ttf │ │ │ └── open-iconic.woff │ └── site.css │ ├── favicon.ico │ ├── index.html │ └── sample-data │ └── weather.json ├── BlazorSecurityDemo.sln ├── IdentityProvider ├── AspIdUsers.db ├── Config.cs ├── Data │ ├── ApplicationDbContext.cs │ └── Migrations │ │ ├── 20180109192453_CreateIdentitySchema.Designer.cs │ │ ├── 20180109192453_CreateIdentitySchema.cs │ │ └── ApplicationDbContextModelSnapshot.cs ├── IdentityProvider.csproj ├── Models │ └── ApplicationUser.cs ├── ProfileWithRoleIdentityResource.cs ├── 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 ├── SeedData.cs ├── Startup.cs ├── Views │ ├── Account │ │ ├── AccessDenied.cshtml │ │ ├── LoggedOut.cshtml │ │ ├── Login.cshtml │ │ └── Logout.cshtml │ ├── Consent │ │ └── Index.cshtml │ ├── Device │ │ ├── Success.cshtml │ │ ├── UserCodeCapture.cshtml │ │ └── UserCodeConfirmation.cshtml │ ├── Diagnostics │ │ └── Index.cshtml │ ├── Grants │ │ └── Index.cshtml │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── Redirect.cshtml │ │ ├── _Layout.cshtml │ │ ├── _ScopeListItem.cshtml │ │ └── _ValidationSummary.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.json ├── tempkey.rsa ├── updateUI.ps1 └── wwwroot │ ├── css │ ├── site.css │ ├── site.less │ └── site.min.css │ ├── favicon.ico │ ├── icon.jpg │ ├── icon.png │ ├── 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 ├── StartupSettings.png ├── WeatherApi ├── Controllers │ └── WeatherForecastController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WeatherApi.csproj ├── WeatherForecast.cs ├── appsettings.Development.json └── appsettings.json └── readme.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /BlazorClient/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @if (context.User.Identity.IsAuthenticated) 7 | { 8 |

Sorry you are not authorized

9 | } 10 | else 11 | { 12 | 13 | } 14 |
15 |
16 |
17 | 18 | 19 |

Sorry, there's nothing at this address.

20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /BlazorClient/BlazorClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 3.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /BlazorClient/Pages/Authentication.razor: -------------------------------------------------------------------------------- 1 | @page "/authentication/{action}" 2 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 3 | 4 | 5 | @code{ 6 | [Parameter] public string Action { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /BlazorClient/Pages/Claims.razor: -------------------------------------------------------------------------------- 1 | @page "/claims" 2 | 3 | 4 | 5 |

Hello @context.User.Identity.Name, here's the list of your claims:

6 |
    7 | @foreach (var claim in context.User.Claims) 8 | { 9 |
  • @claim.Type: @claim.Value
  • 10 | } 11 |
12 |
13 | 14 |

I'm sorry, I can't display any claims until you log in

15 |
16 |
-------------------------------------------------------------------------------- /BlazorClient/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 |

Counter

4 | 5 |

Current count: @currentCount

6 | 7 | 8 | 9 | @code { 10 | private int currentCount = 0; 11 | 12 | private void IncrementCount() 13 | { 14 | currentCount++; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BlazorClient/Pages/FetchData.razor: -------------------------------------------------------------------------------- 1 | @page "/fetchdata" 2 | @inject HttpClient Http 3 | @attribute [Authorize(Roles = "Admin")] 4 | 5 |

Weather forecast

6 | 7 |

This component demonstrates fetching data from the server.

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

Loading...

12 | } 13 | else 14 | { 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @foreach (var forecast in forecasts) 26 | { 27 | 28 | 29 | 30 | 31 | 32 | 33 | } 34 | 35 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
36 | } 37 | 38 | @code { 39 | private WeatherForecast[] forecasts; 40 | 41 | protected override async Task OnInitializedAsync() 42 | { 43 | forecasts = await Http.GetFromJsonAsync("https://localhost:5002/WeatherForecast"); 44 | } 45 | 46 | public class WeatherForecast 47 | { 48 | public DateTime Date { get; set; } 49 | 50 | public int TemperatureC { get; set; } 51 | 52 | public string Summary { get; set; } 53 | 54 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /BlazorClient/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

Hello, world!

4 | 5 | Welcome to your new app. 6 | 7 | 8 | -------------------------------------------------------------------------------- /BlazorClient/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorClient.Security; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlazorClient 10 | { 11 | public class Program 12 | { 13 | public static async Task Main(string[] args) 14 | { 15 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 16 | builder.RootComponents.Add("app"); 17 | 18 | builder.Services.AddHttpClient("api") 19 | .AddHttpMessageHandler(sp => 20 | { 21 | var handler = sp.GetService() 22 | .ConfigureHandler( 23 | authorizedUrls: new[] { "https://localhost:5002" }, 24 | scopes: new[] { "weatherapi" }); 25 | 26 | return handler; 27 | }); 28 | 29 | builder.Services.AddScoped(sp => sp.GetService().CreateClient("api")); 30 | 31 | builder.Services 32 | .AddOidcAuthentication(options => 33 | { 34 | builder.Configuration.Bind("oidc", options.ProviderOptions); 35 | options.UserOptions.RoleClaim = "role"; 36 | }) 37 | .AddAccountClaimsPrincipalFactory>(); 38 | 39 | await builder.Build().RunAsync(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BlazorClient/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "BlazorClient": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 7 | "applicationUrl": "https://localhost:5001", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazorClient/Security/ArrayClaimsPrincipalFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorClient.Security 9 | { 10 | public class ArrayClaimsPrincipalFactory : AccountClaimsPrincipalFactory where TAccount:RemoteUserAccount 11 | { 12 | public ArrayClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) 13 | : base(accessor) 14 | { } 15 | 16 | 17 | // when a user belongs to multiple roles, IS4 returns a single claim with a serialised array of values 18 | // this class improves the original factory by deserializing the claims in the correct way 19 | public async override ValueTask CreateUserAsync(TAccount account, RemoteAuthenticationUserOptions options) 20 | { 21 | var user = await base.CreateUserAsync(account, options); 22 | 23 | var claimsIdentity = (ClaimsIdentity)user.Identity; 24 | 25 | if (account != null) 26 | { 27 | foreach (var kvp in account.AdditionalProperties) 28 | { 29 | var name = kvp.Key; 30 | var value = kvp.Value; 31 | if (value != null && 32 | (value is JsonElement element && element.ValueKind == JsonValueKind.Array)) 33 | { 34 | claimsIdentity.RemoveClaim(claimsIdentity.FindFirst(kvp.Key)); 35 | 36 | var claims = element.EnumerateArray() 37 | .Select(x => new Claim(kvp.Key, x.ToString())); 38 | 39 | claimsIdentity.AddClaims(claims); 40 | } 41 | } 42 | } 43 | 44 | return user; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlazorClient/Shared/LoginDisplay.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Authorization 2 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 3 | 4 | @inject NavigationManager Navigation 5 | @inject SignOutSessionStateManager SignOutManager 6 | 7 | 8 | 9 | Hello, @context.User.Identity.Name! 10 | 11 | 12 | 13 | Log in 14 | 15 | 16 | 17 | @code{ 18 | private async Task BeginSignOut(MouseEventArgs args) 19 | { 20 | await SignOutManager.SetSignOutState(); 21 | Navigation.NavigateTo("authentication/logout"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BlazorClient/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 6 | 7 |
8 |
9 | 10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 | -------------------------------------------------------------------------------- /BlazorClient/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 31 |
32 | 33 | @code { 34 | private bool collapseNavMenu = true; 35 | 36 | private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; 37 | 38 | private void ToggleNavMenu() 39 | { 40 | collapseNavMenu = !collapseNavMenu; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BlazorClient/Shared/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager Navigation 2 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 3 | @code { 4 | protected override void OnInitialized() 5 | { 6 | Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorClient/Shared/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 |  11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /BlazorClient/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Components.Authorization 3 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 4 | @using Microsoft.AspNetCore.Authorization 5 | @using Microsoft.AspNetCore.Components.Forms 6 | @using Microsoft.AspNetCore.Components.Routing 7 | @using Microsoft.AspNetCore.Components.Web 8 | @using Microsoft.JSInterop 9 | @using BlazorClient 10 | @using BlazorClient.Shared 11 | @using System.Net.Http.Json -------------------------------------------------------------------------------- /BlazorClient/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "oidc": { 3 | "Authority": "https://localhost:5000/", 4 | "ClientId": "blazor", 5 | "DefaultScopes": [ 6 | "openid", 7 | "profile", 8 | "email", 9 | "weatherapi" 10 | ], 11 | "PostLogoutRedirectUri": "/", 12 | "ResponseType": "code" 13 | } 14 | } -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/BlazorClient/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /BlazorClient/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | app { 18 | position: relative; 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | 23 | .top-row { 24 | height: 3.5rem; 25 | display: flex; 26 | align-items: center; 27 | } 28 | 29 | .main { 30 | flex: 1; 31 | } 32 | 33 | .main .top-row { 34 | background-color: #f7f7f7; 35 | border-bottom: 1px solid #d6d5d5; 36 | justify-content: flex-end; 37 | } 38 | 39 | .main .top-row > a, .main .top-row .btn-link { 40 | white-space: nowrap; 41 | margin-left: 1.5rem; 42 | } 43 | 44 | .main .top-row a:first-child { 45 | overflow: hidden; 46 | text-overflow: ellipsis; 47 | } 48 | 49 | .sidebar { 50 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 51 | } 52 | 53 | .sidebar .top-row { 54 | background-color: rgba(0,0,0,0.4); 55 | } 56 | 57 | .sidebar .navbar-brand { 58 | font-size: 1.1rem; 59 | } 60 | 61 | .sidebar .oi { 62 | width: 2rem; 63 | font-size: 1.1rem; 64 | vertical-align: text-top; 65 | top: -2px; 66 | } 67 | 68 | .sidebar .nav-item { 69 | font-size: 0.9rem; 70 | padding-bottom: 0.5rem; 71 | } 72 | 73 | .sidebar .nav-item:first-of-type { 74 | padding-top: 1rem; 75 | } 76 | 77 | .sidebar .nav-item:last-of-type { 78 | padding-bottom: 1rem; 79 | } 80 | 81 | .sidebar .nav-item a { 82 | color: #d7d7d7; 83 | border-radius: 4px; 84 | height: 3rem; 85 | display: flex; 86 | align-items: center; 87 | line-height: 3rem; 88 | } 89 | 90 | .sidebar .nav-item a.active { 91 | background-color: rgba(255,255,255,0.25); 92 | color: white; 93 | } 94 | 95 | .sidebar .nav-item a:hover { 96 | background-color: rgba(255,255,255,0.1); 97 | color: white; 98 | } 99 | 100 | .content { 101 | padding-top: 1.1rem; 102 | } 103 | 104 | .navbar-toggler { 105 | background-color: rgba(255, 255, 255, 0.1); 106 | } 107 | 108 | .valid.modified:not([type=checkbox]) { 109 | outline: 1px solid #26b050; 110 | } 111 | 112 | .invalid { 113 | outline: 1px solid red; 114 | } 115 | 116 | .validation-message { 117 | color: red; 118 | } 119 | 120 | #blazor-error-ui { 121 | background: lightyellow; 122 | bottom: 0; 123 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 124 | display: none; 125 | left: 0; 126 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 127 | position: fixed; 128 | width: 100%; 129 | z-index: 1000; 130 | } 131 | 132 | #blazor-error-ui .dismiss { 133 | cursor: pointer; 134 | position: absolute; 135 | right: 0.75rem; 136 | top: 0.5rem; 137 | } 138 | 139 | @media (max-width: 767.98px) { 140 | .main .top-row:not(.auth) { 141 | display: none; 142 | } 143 | 144 | .main .top-row.auth { 145 | justify-content: space-between; 146 | } 147 | 148 | .main .top-row a, .main .top-row .btn-link { 149 | margin-left: 0; 150 | } 151 | } 152 | 153 | @media (min-width: 768px) { 154 | app { 155 | flex-direction: row; 156 | } 157 | 158 | .sidebar { 159 | width: 250px; 160 | height: 100vh; 161 | position: sticky; 162 | top: 0; 163 | } 164 | 165 | .main .top-row { 166 | position: sticky; 167 | top: 0; 168 | } 169 | 170 | .main > div { 171 | padding-left: 2rem !important; 172 | padding-right: 1.5rem !important; 173 | } 174 | 175 | .navbar-toggler { 176 | display: none; 177 | } 178 | 179 | .sidebar .collapse { 180 | /* Never collapse the sidebar for wide screens */ 181 | display: block; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /BlazorClient/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/BlazorClient/wwwroot/favicon.ico -------------------------------------------------------------------------------- /BlazorClient/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BlazorClient 8 | 9 | 10 | 11 | 12 | 13 | 14 | Loading... 15 | 16 |
17 | An unhandled error has occurred. 18 | Reload 19 | 🗙 20 |
21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /BlazorClient/wwwroot/sample-data/weather.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "date": "2018-05-06", 4 | "temperatureC": 1, 5 | "summary": "Freezing", 6 | "temperatureF": 33 7 | }, 8 | { 9 | "date": "2018-05-07", 10 | "temperatureC": 14, 11 | "summary": "Bracing", 12 | "temperatureF": 57 13 | }, 14 | { 15 | "date": "2018-05-08", 16 | "temperatureC": -13, 17 | "summary": "Freezing", 18 | "temperatureF": 9 19 | }, 20 | { 21 | "date": "2018-05-09", 22 | "temperatureC": -16, 23 | "summary": "Balmy", 24 | "temperatureF": 4 25 | }, 26 | { 27 | "date": "2018-05-10", 28 | "temperatureC": -2, 29 | "summary": "Chilly", 30 | "temperatureF": 29 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /BlazorSecurityDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29924.181 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentityProvider", "IdentityProvider\IdentityProvider.csproj", "{BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "BlazorClient\BlazorClient.csproj", "{ED4426C1-2598-432B-91D0-654EAE091E79}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WeatherApi", "weatherapi\WeatherApi.csproj", "{3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Debug|x64.ActiveCfg = Debug|Any CPU 25 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Debug|x64.Build.0 = Debug|Any CPU 26 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Debug|x86.ActiveCfg = Debug|Any CPU 27 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Debug|x86.Build.0 = Debug|Any CPU 28 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Release|x64.ActiveCfg = Release|Any CPU 31 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Release|x64.Build.0 = Release|Any CPU 32 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Release|x86.ActiveCfg = Release|Any CPU 33 | {BC94B282-E0A1-41E6-AD5A-EADB374FBAC5}.Release|x86.Build.0 = Release|Any CPU 34 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Debug|x64.ActiveCfg = Debug|Any CPU 37 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Debug|x64.Build.0 = Debug|Any CPU 38 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Debug|x86.ActiveCfg = Debug|Any CPU 39 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Debug|x86.Build.0 = Debug|Any CPU 40 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Release|x64.ActiveCfg = Release|Any CPU 43 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Release|x64.Build.0 = Release|Any CPU 44 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Release|x86.ActiveCfg = Release|Any CPU 45 | {ED4426C1-2598-432B-91D0-654EAE091E79}.Release|x86.Build.0 = Release|Any CPU 46 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Debug|x64.ActiveCfg = Debug|Any CPU 49 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Debug|x64.Build.0 = Debug|Any CPU 50 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Debug|x86.ActiveCfg = Debug|Any CPU 51 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Debug|x86.Build.0 = Debug|Any CPU 52 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Release|x64.ActiveCfg = Release|Any CPU 55 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Release|x64.Build.0 = Release|Any CPU 56 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Release|x86.ActiveCfg = Release|Any CPU 57 | {3CCFCC71-5C4D-4F8F-8EF3-63C585AA6700}.Release|x86.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {5710F0D4-02DE-4566-9859-7E375928CB8C} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /IdentityProvider/AspIdUsers.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/AspIdUsers.db -------------------------------------------------------------------------------- /IdentityProvider/Config.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using IdentityServer4.Models; 7 | using System.Collections.Generic; 8 | 9 | namespace IdentityProvider 10 | { 11 | public static class Config 12 | { 13 | public static IEnumerable Ids => 14 | new IdentityResource[] 15 | { 16 | new IdentityResources.OpenId(), 17 | 18 | // let's include the role claim in the profile 19 | new ProfileWithRoleIdentityResource(), 20 | new IdentityResources.Email() 21 | }; 22 | 23 | 24 | public static IEnumerable Apis => 25 | new ApiResource[] 26 | { 27 | // the api requires the role claim 28 | new ApiResource("weatherapi", "The Weather API", new[] { JwtClaimTypes.Role }) 29 | }; 30 | 31 | 32 | public static IEnumerable Clients => 33 | new Client[] 34 | { 35 | new Client 36 | { 37 | ClientId = "blazor", 38 | AllowedGrantTypes = GrantTypes.Code, 39 | RequirePkce = true, 40 | RequireClientSecret = false, 41 | AllowedCorsOrigins = { "https://localhost:5001" }, 42 | AllowedScopes = { "openid", "profile", "email", "weatherapi" }, 43 | RedirectUris = { "https://localhost:5001/authentication/login-callback" }, 44 | PostLogoutRedirectUris = { "https://localhost:5001/" }, 45 | Enabled = true 46 | }, 47 | }; 48 | } 49 | } -------------------------------------------------------------------------------- /IdentityProvider/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | using IdentityProvider.Models; 4 | 5 | namespace IdentityProvider.Data 6 | { 7 | public class ApplicationDbContext : IdentityDbContext 8 | { 9 | public ApplicationDbContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | protected override void OnModelCreating(ModelBuilder builder) 15 | { 16 | base.OnModelCreating(builder); 17 | // Customize the ASP.NET Identity model and override the defaults if needed. 18 | // For example, you can rename the ASP.NET Identity table names and more. 19 | // Add your customizations after calling base.OnModelCreating(builder); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /IdentityProvider/Data/Migrations/20180109192453_CreateIdentitySchema.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using IdentityProvider.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.EntityFrameworkCore.Storage.Internal; 9 | using System; 10 | 11 | namespace IdentityProvider.Data.Migrations 12 | { 13 | [DbContext(typeof(ApplicationDbContext))] 14 | [Migration("20180109192453_CreateIdentitySchema")] 15 | partial class CreateIdentitySchema 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "2.0.1-rtm-125"); 22 | 23 | modelBuilder.Entity("IdentityProvider.Models.ApplicationUser", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd(); 27 | 28 | b.Property("AccessFailedCount"); 29 | 30 | b.Property("ConcurrencyStamp") 31 | .IsConcurrencyToken(); 32 | 33 | b.Property("Email") 34 | .HasMaxLength(256); 35 | 36 | b.Property("EmailConfirmed"); 37 | 38 | b.Property("LockoutEnabled"); 39 | 40 | b.Property("LockoutEnd"); 41 | 42 | b.Property("NormalizedEmail") 43 | .HasMaxLength(256); 44 | 45 | b.Property("NormalizedUserName") 46 | .HasMaxLength(256); 47 | 48 | b.Property("PasswordHash"); 49 | 50 | b.Property("PhoneNumber"); 51 | 52 | b.Property("PhoneNumberConfirmed"); 53 | 54 | b.Property("SecurityStamp"); 55 | 56 | b.Property("TwoFactorEnabled"); 57 | 58 | b.Property("UserName") 59 | .HasMaxLength(256); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.HasIndex("NormalizedEmail") 64 | .HasName("EmailIndex"); 65 | 66 | b.HasIndex("NormalizedUserName") 67 | .IsUnique() 68 | .HasName("UserNameIndex"); 69 | 70 | b.ToTable("AspNetUsers"); 71 | }); 72 | 73 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 74 | { 75 | b.Property("Id") 76 | .ValueGeneratedOnAdd(); 77 | 78 | b.Property("ConcurrencyStamp") 79 | .IsConcurrencyToken(); 80 | 81 | b.Property("Name") 82 | .HasMaxLength(256); 83 | 84 | b.Property("NormalizedName") 85 | .HasMaxLength(256); 86 | 87 | b.HasKey("Id"); 88 | 89 | b.HasIndex("NormalizedName") 90 | .IsUnique() 91 | .HasName("RoleNameIndex"); 92 | 93 | b.ToTable("AspNetRoles"); 94 | }); 95 | 96 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 97 | { 98 | b.Property("Id") 99 | .ValueGeneratedOnAdd(); 100 | 101 | b.Property("ClaimType"); 102 | 103 | b.Property("ClaimValue"); 104 | 105 | b.Property("RoleId") 106 | .IsRequired(); 107 | 108 | b.HasKey("Id"); 109 | 110 | b.HasIndex("RoleId"); 111 | 112 | b.ToTable("AspNetRoleClaims"); 113 | }); 114 | 115 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 116 | { 117 | b.Property("Id") 118 | .ValueGeneratedOnAdd(); 119 | 120 | b.Property("ClaimType"); 121 | 122 | b.Property("ClaimValue"); 123 | 124 | b.Property("UserId") 125 | .IsRequired(); 126 | 127 | b.HasKey("Id"); 128 | 129 | b.HasIndex("UserId"); 130 | 131 | b.ToTable("AspNetUserClaims"); 132 | }); 133 | 134 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 135 | { 136 | b.Property("LoginProvider"); 137 | 138 | b.Property("ProviderKey"); 139 | 140 | b.Property("ProviderDisplayName"); 141 | 142 | b.Property("UserId") 143 | .IsRequired(); 144 | 145 | b.HasKey("LoginProvider", "ProviderKey"); 146 | 147 | b.HasIndex("UserId"); 148 | 149 | b.ToTable("AspNetUserLogins"); 150 | }); 151 | 152 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 153 | { 154 | b.Property("UserId"); 155 | 156 | b.Property("RoleId"); 157 | 158 | b.HasKey("UserId", "RoleId"); 159 | 160 | b.HasIndex("RoleId"); 161 | 162 | b.ToTable("AspNetUserRoles"); 163 | }); 164 | 165 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 166 | { 167 | b.Property("UserId"); 168 | 169 | b.Property("LoginProvider"); 170 | 171 | b.Property("Name"); 172 | 173 | b.Property("Value"); 174 | 175 | b.HasKey("UserId", "LoginProvider", "Name"); 176 | 177 | b.ToTable("AspNetUserTokens"); 178 | }); 179 | 180 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 181 | { 182 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 183 | .WithMany() 184 | .HasForeignKey("RoleId") 185 | .OnDelete(DeleteBehavior.Cascade); 186 | }); 187 | 188 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 189 | { 190 | b.HasOne("IdentityProvider.Models.ApplicationUser") 191 | .WithMany() 192 | .HasForeignKey("UserId") 193 | .OnDelete(DeleteBehavior.Cascade); 194 | }); 195 | 196 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 197 | { 198 | b.HasOne("IdentityProvider.Models.ApplicationUser") 199 | .WithMany() 200 | .HasForeignKey("UserId") 201 | .OnDelete(DeleteBehavior.Cascade); 202 | }); 203 | 204 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 205 | { 206 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 207 | .WithMany() 208 | .HasForeignKey("RoleId") 209 | .OnDelete(DeleteBehavior.Cascade); 210 | 211 | b.HasOne("IdentityProvider.Models.ApplicationUser") 212 | .WithMany() 213 | .HasForeignKey("UserId") 214 | .OnDelete(DeleteBehavior.Cascade); 215 | }); 216 | 217 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 218 | { 219 | b.HasOne("IdentityProvider.Models.ApplicationUser") 220 | .WithMany() 221 | .HasForeignKey("UserId") 222 | .OnDelete(DeleteBehavior.Cascade); 223 | }); 224 | #pragma warning restore 612, 618 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /IdentityProvider/Data/Migrations/20180109192453_CreateIdentitySchema.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | 4 | namespace IdentityProvider.Data.Migrations 5 | { 6 | public partial class CreateIdentitySchema : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "AspNetRoles", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | ConcurrencyStamp = table.Column(nullable: true), 16 | Name = table.Column(maxLength: 256, nullable: true), 17 | NormalizedName = table.Column(maxLength: 256, nullable: true) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_AspNetRoles", x => x.Id); 22 | }); 23 | 24 | migrationBuilder.CreateTable( 25 | name: "AspNetUsers", 26 | columns: table => new 27 | { 28 | Id = table.Column(nullable: false), 29 | AccessFailedCount = table.Column(nullable: false), 30 | ConcurrencyStamp = table.Column(nullable: true), 31 | Email = table.Column(maxLength: 256, nullable: true), 32 | EmailConfirmed = table.Column(nullable: false), 33 | LockoutEnabled = table.Column(nullable: false), 34 | LockoutEnd = table.Column(nullable: true), 35 | NormalizedEmail = table.Column(maxLength: 256, nullable: true), 36 | NormalizedUserName = table.Column(maxLength: 256, nullable: true), 37 | PasswordHash = table.Column(nullable: true), 38 | PhoneNumber = table.Column(nullable: true), 39 | PhoneNumberConfirmed = table.Column(nullable: false), 40 | SecurityStamp = table.Column(nullable: true), 41 | TwoFactorEnabled = table.Column(nullable: false), 42 | UserName = table.Column(maxLength: 256, nullable: true) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_AspNetUsers", x => x.Id); 47 | }); 48 | 49 | migrationBuilder.CreateTable( 50 | name: "AspNetRoleClaims", 51 | columns: table => new 52 | { 53 | Id = table.Column(nullable: false) 54 | .Annotation("Sqlite:Autoincrement", true), 55 | ClaimType = table.Column(nullable: true), 56 | ClaimValue = table.Column(nullable: true), 57 | RoleId = table.Column(nullable: false) 58 | }, 59 | constraints: table => 60 | { 61 | table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); 62 | table.ForeignKey( 63 | name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", 64 | column: x => x.RoleId, 65 | principalTable: "AspNetRoles", 66 | principalColumn: "Id", 67 | onDelete: ReferentialAction.Cascade); 68 | }); 69 | 70 | migrationBuilder.CreateTable( 71 | name: "AspNetUserClaims", 72 | columns: table => new 73 | { 74 | Id = table.Column(nullable: false) 75 | .Annotation("Sqlite:Autoincrement", true), 76 | ClaimType = table.Column(nullable: true), 77 | ClaimValue = table.Column(nullable: true), 78 | UserId = table.Column(nullable: false) 79 | }, 80 | constraints: table => 81 | { 82 | table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); 83 | table.ForeignKey( 84 | name: "FK_AspNetUserClaims_AspNetUsers_UserId", 85 | column: x => x.UserId, 86 | principalTable: "AspNetUsers", 87 | principalColumn: "Id", 88 | onDelete: ReferentialAction.Cascade); 89 | }); 90 | 91 | migrationBuilder.CreateTable( 92 | name: "AspNetUserLogins", 93 | columns: table => new 94 | { 95 | LoginProvider = table.Column(nullable: false), 96 | ProviderKey = table.Column(nullable: false), 97 | ProviderDisplayName = table.Column(nullable: true), 98 | UserId = table.Column(nullable: false) 99 | }, 100 | constraints: table => 101 | { 102 | table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); 103 | table.ForeignKey( 104 | name: "FK_AspNetUserLogins_AspNetUsers_UserId", 105 | column: x => x.UserId, 106 | principalTable: "AspNetUsers", 107 | principalColumn: "Id", 108 | onDelete: ReferentialAction.Cascade); 109 | }); 110 | 111 | migrationBuilder.CreateTable( 112 | name: "AspNetUserRoles", 113 | columns: table => new 114 | { 115 | UserId = table.Column(nullable: false), 116 | RoleId = table.Column(nullable: false) 117 | }, 118 | constraints: table => 119 | { 120 | table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); 121 | table.ForeignKey( 122 | name: "FK_AspNetUserRoles_AspNetRoles_RoleId", 123 | column: x => x.RoleId, 124 | principalTable: "AspNetRoles", 125 | principalColumn: "Id", 126 | onDelete: ReferentialAction.Cascade); 127 | table.ForeignKey( 128 | name: "FK_AspNetUserRoles_AspNetUsers_UserId", 129 | column: x => x.UserId, 130 | principalTable: "AspNetUsers", 131 | principalColumn: "Id", 132 | onDelete: ReferentialAction.Cascade); 133 | }); 134 | 135 | migrationBuilder.CreateTable( 136 | name: "AspNetUserTokens", 137 | columns: table => new 138 | { 139 | UserId = table.Column(nullable: false), 140 | LoginProvider = table.Column(nullable: false), 141 | Name = table.Column(nullable: false), 142 | Value = table.Column(nullable: true) 143 | }, 144 | constraints: table => 145 | { 146 | table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); 147 | table.ForeignKey( 148 | name: "FK_AspNetUserTokens_AspNetUsers_UserId", 149 | column: x => x.UserId, 150 | principalTable: "AspNetUsers", 151 | principalColumn: "Id", 152 | onDelete: ReferentialAction.Cascade); 153 | }); 154 | 155 | migrationBuilder.CreateIndex( 156 | name: "IX_AspNetRoleClaims_RoleId", 157 | table: "AspNetRoleClaims", 158 | column: "RoleId"); 159 | 160 | migrationBuilder.CreateIndex( 161 | name: "RoleNameIndex", 162 | table: "AspNetRoles", 163 | column: "NormalizedName", 164 | unique: true); 165 | 166 | migrationBuilder.CreateIndex( 167 | name: "IX_AspNetUserClaims_UserId", 168 | table: "AspNetUserClaims", 169 | column: "UserId"); 170 | 171 | migrationBuilder.CreateIndex( 172 | name: "IX_AspNetUserLogins_UserId", 173 | table: "AspNetUserLogins", 174 | column: "UserId"); 175 | 176 | migrationBuilder.CreateIndex( 177 | name: "IX_AspNetUserRoles_RoleId", 178 | table: "AspNetUserRoles", 179 | column: "RoleId"); 180 | 181 | migrationBuilder.CreateIndex( 182 | name: "EmailIndex", 183 | table: "AspNetUsers", 184 | column: "NormalizedEmail"); 185 | 186 | migrationBuilder.CreateIndex( 187 | name: "UserNameIndex", 188 | table: "AspNetUsers", 189 | column: "NormalizedUserName", 190 | unique: true); 191 | } 192 | 193 | protected override void Down(MigrationBuilder migrationBuilder) 194 | { 195 | migrationBuilder.DropTable( 196 | name: "AspNetRoleClaims"); 197 | 198 | migrationBuilder.DropTable( 199 | name: "AspNetUserClaims"); 200 | 201 | migrationBuilder.DropTable( 202 | name: "AspNetUserLogins"); 203 | 204 | migrationBuilder.DropTable( 205 | name: "AspNetUserRoles"); 206 | 207 | migrationBuilder.DropTable( 208 | name: "AspNetUserTokens"); 209 | 210 | migrationBuilder.DropTable( 211 | name: "AspNetRoles"); 212 | 213 | migrationBuilder.DropTable( 214 | name: "AspNetUsers"); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /IdentityProvider/Data/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using IdentityProvider.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.EntityFrameworkCore.Storage.Internal; 9 | using System; 10 | 11 | namespace IdentityProvider.Data.Migrations 12 | { 13 | [DbContext(typeof(ApplicationDbContext))] 14 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.0.1-rtm-125"); 21 | 22 | modelBuilder.Entity("IdentityProvider.Models.ApplicationUser", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("AccessFailedCount"); 28 | 29 | b.Property("ConcurrencyStamp") 30 | .IsConcurrencyToken(); 31 | 32 | b.Property("Email") 33 | .HasMaxLength(256); 34 | 35 | b.Property("EmailConfirmed"); 36 | 37 | b.Property("LockoutEnabled"); 38 | 39 | b.Property("LockoutEnd"); 40 | 41 | b.Property("NormalizedEmail") 42 | .HasMaxLength(256); 43 | 44 | b.Property("NormalizedUserName") 45 | .HasMaxLength(256); 46 | 47 | b.Property("PasswordHash"); 48 | 49 | b.Property("PhoneNumber"); 50 | 51 | b.Property("PhoneNumberConfirmed"); 52 | 53 | b.Property("SecurityStamp"); 54 | 55 | b.Property("TwoFactorEnabled"); 56 | 57 | b.Property("UserName") 58 | .HasMaxLength(256); 59 | 60 | b.HasKey("Id"); 61 | 62 | b.HasIndex("NormalizedEmail") 63 | .HasName("EmailIndex"); 64 | 65 | b.HasIndex("NormalizedUserName") 66 | .IsUnique() 67 | .HasName("UserNameIndex"); 68 | 69 | b.ToTable("AspNetUsers"); 70 | }); 71 | 72 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 73 | { 74 | b.Property("Id") 75 | .ValueGeneratedOnAdd(); 76 | 77 | b.Property("ConcurrencyStamp") 78 | .IsConcurrencyToken(); 79 | 80 | b.Property("Name") 81 | .HasMaxLength(256); 82 | 83 | b.Property("NormalizedName") 84 | .HasMaxLength(256); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.HasIndex("NormalizedName") 89 | .IsUnique() 90 | .HasName("RoleNameIndex"); 91 | 92 | b.ToTable("AspNetRoles"); 93 | }); 94 | 95 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 96 | { 97 | b.Property("Id") 98 | .ValueGeneratedOnAdd(); 99 | 100 | b.Property("ClaimType"); 101 | 102 | b.Property("ClaimValue"); 103 | 104 | b.Property("RoleId") 105 | .IsRequired(); 106 | 107 | b.HasKey("Id"); 108 | 109 | b.HasIndex("RoleId"); 110 | 111 | b.ToTable("AspNetRoleClaims"); 112 | }); 113 | 114 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 115 | { 116 | b.Property("Id") 117 | .ValueGeneratedOnAdd(); 118 | 119 | b.Property("ClaimType"); 120 | 121 | b.Property("ClaimValue"); 122 | 123 | b.Property("UserId") 124 | .IsRequired(); 125 | 126 | b.HasKey("Id"); 127 | 128 | b.HasIndex("UserId"); 129 | 130 | b.ToTable("AspNetUserClaims"); 131 | }); 132 | 133 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 134 | { 135 | b.Property("LoginProvider"); 136 | 137 | b.Property("ProviderKey"); 138 | 139 | b.Property("ProviderDisplayName"); 140 | 141 | b.Property("UserId") 142 | .IsRequired(); 143 | 144 | b.HasKey("LoginProvider", "ProviderKey"); 145 | 146 | b.HasIndex("UserId"); 147 | 148 | b.ToTable("AspNetUserLogins"); 149 | }); 150 | 151 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 152 | { 153 | b.Property("UserId"); 154 | 155 | b.Property("RoleId"); 156 | 157 | b.HasKey("UserId", "RoleId"); 158 | 159 | b.HasIndex("RoleId"); 160 | 161 | b.ToTable("AspNetUserRoles"); 162 | }); 163 | 164 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 165 | { 166 | b.Property("UserId"); 167 | 168 | b.Property("LoginProvider"); 169 | 170 | b.Property("Name"); 171 | 172 | b.Property("Value"); 173 | 174 | b.HasKey("UserId", "LoginProvider", "Name"); 175 | 176 | b.ToTable("AspNetUserTokens"); 177 | }); 178 | 179 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 180 | { 181 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 182 | .WithMany() 183 | .HasForeignKey("RoleId") 184 | .OnDelete(DeleteBehavior.Cascade); 185 | }); 186 | 187 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 188 | { 189 | b.HasOne("IdentityProvider.Models.ApplicationUser") 190 | .WithMany() 191 | .HasForeignKey("UserId") 192 | .OnDelete(DeleteBehavior.Cascade); 193 | }); 194 | 195 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 196 | { 197 | b.HasOne("IdentityProvider.Models.ApplicationUser") 198 | .WithMany() 199 | .HasForeignKey("UserId") 200 | .OnDelete(DeleteBehavior.Cascade); 201 | }); 202 | 203 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 204 | { 205 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 206 | .WithMany() 207 | .HasForeignKey("RoleId") 208 | .OnDelete(DeleteBehavior.Cascade); 209 | 210 | b.HasOne("IdentityProvider.Models.ApplicationUser") 211 | .WithMany() 212 | .HasForeignKey("UserId") 213 | .OnDelete(DeleteBehavior.Cascade); 214 | }); 215 | 216 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 217 | { 218 | b.HasOne("IdentityProvider.Models.ApplicationUser") 219 | .WithMany() 220 | .HasForeignKey("UserId") 221 | .OnDelete(DeleteBehavior.Cascade); 222 | }); 223 | #pragma warning restore 612, 618 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /IdentityProvider/IdentityProvider.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /IdentityProvider/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace IdentityProvider.Models 4 | { 5 | // Add profile data for application users by adding properties to the ApplicationUser class 6 | public class ApplicationUser : IdentityUser 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /IdentityProvider/ProfileWithRoleIdentityResource.cs: -------------------------------------------------------------------------------- 1 | using IdentityModel; 2 | using IdentityServer4.Models; 3 | 4 | namespace IdentityProvider 5 | { 6 | public class ProfileWithRoleIdentityResource : IdentityResources.Profile 7 | { 8 | public ProfileWithRoleIdentityResource() 9 | { 10 | this.UserClaims.Add(JwtClaimTypes.Role); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /IdentityProvider/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Serilog; 10 | using Serilog.Events; 11 | using Serilog.Sinks.SystemConsole.Themes; 12 | using System; 13 | using System.Linq; 14 | 15 | namespace IdentityProvider 16 | { 17 | public class Program 18 | { 19 | public static int Main(string[] args) 20 | { 21 | Log.Logger = new LoggerConfiguration() 22 | .MinimumLevel.Debug() 23 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 24 | .MinimumLevel.Override("System", LogEventLevel.Warning) 25 | .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) 26 | .Enrich.FromLogContext() 27 | // uncomment to write to Azure diagnostics stream 28 | //.WriteTo.File( 29 | // @"D:\home\LogFiles\Application\identityserver.txt", 30 | // fileSizeLimitBytes: 1_000_000, 31 | // rollOnFileSizeLimit: true, 32 | // shared: true, 33 | // flushToDiskInterval: TimeSpan.FromSeconds(1)) 34 | .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate) 35 | .CreateLogger(); 36 | 37 | try 38 | { 39 | var seed = args.Contains("/seed"); 40 | if (seed) 41 | { 42 | args = args.Except(new[] { "/seed" }).ToArray(); 43 | } 44 | 45 | var host = CreateHostBuilder(args).Build(); 46 | 47 | if (seed) 48 | { 49 | Log.Information("Seeding database..."); 50 | var config = host.Services.GetRequiredService(); 51 | var connectionString = config.GetConnectionString("DefaultConnection"); 52 | SeedData.EnsureSeedData(connectionString); 53 | Log.Information("Done seeding database."); 54 | return 0; 55 | } 56 | 57 | Log.Information("Starting host..."); 58 | host.Run(); 59 | return 0; 60 | } 61 | catch (Exception ex) 62 | { 63 | Log.Fatal(ex, "Host terminated unexpectedly."); 64 | return 1; 65 | } 66 | finally 67 | { 68 | Log.CloseAndFlush(); 69 | } 70 | } 71 | 72 | public static IHostBuilder CreateHostBuilder(string[] args) => 73 | Host.CreateDefaultBuilder(args) 74 | .ConfigureWebHostDefaults(webBuilder => 75 | { 76 | webBuilder.UseStartup(); 77 | webBuilder.UseSerilog(); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /IdentityProvider/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SelfHost": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:5000" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Account/AccountOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class AccountOptions 10 | { 11 | public static bool AllowLocalLogin = true; 12 | public static bool AllowRememberLogin = true; 13 | public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); 14 | 15 | public static bool ShowLogoutPrompt = true; 16 | public static bool AutomaticRedirectAfterSignOut = false; 17 | 18 | // specify the Windows authentication scheme being used 19 | public static readonly string WindowsAuthenticationSchemeName = Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme; 20 | // if user uses windows auth, should we load the groups from windows 21 | public static bool IncludeWindowsGroups = false; 22 | 23 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /IdentityProvider/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 IdentityServer4.Services; 10 | using IdentityServer4.Stores; 11 | using IdentityProvider.Models; 12 | using Microsoft.AspNetCore.Authentication; 13 | using Microsoft.AspNetCore.Authorization; 14 | using Microsoft.AspNetCore.Http; 15 | using Microsoft.AspNetCore.Identity; 16 | using Microsoft.AspNetCore.Mvc; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace IdentityServer4.Quickstart.UI 20 | { 21 | [SecurityHeaders] 22 | [AllowAnonymous] 23 | public class ExternalController : Controller 24 | { 25 | private readonly UserManager _userManager; 26 | private readonly SignInManager _signInManager; 27 | private readonly IIdentityServerInteractionService _interaction; 28 | private readonly IClientStore _clientStore; 29 | private readonly IEventService _events; 30 | private readonly ILogger _logger; 31 | 32 | public ExternalController( 33 | UserManager userManager, 34 | SignInManager signInManager, 35 | IIdentityServerInteractionService interaction, 36 | IClientStore clientStore, 37 | IEventService events, 38 | ILogger logger) 39 | { 40 | _userManager = userManager; 41 | _signInManager = signInManager; 42 | _interaction = interaction; 43 | _clientStore = clientStore; 44 | _events = events; 45 | _logger = logger; 46 | } 47 | 48 | /// 49 | /// initiate roundtrip to external authentication provider 50 | /// 51 | [HttpGet] 52 | public async Task Challenge(string provider, string returnUrl) 53 | { 54 | if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/"; 55 | 56 | // validate returnUrl - either it is a valid OIDC URL or back to a local page 57 | if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false) 58 | { 59 | // user might have clicked on a malicious link - should be logged 60 | throw new Exception("invalid return URL"); 61 | } 62 | 63 | if (AccountOptions.WindowsAuthenticationSchemeName == provider) 64 | { 65 | // windows authentication needs special handling 66 | return await ProcessWindowsLoginAsync(returnUrl); 67 | } 68 | else 69 | { 70 | // start challenge and roundtrip the return URL and scheme 71 | var props = new AuthenticationProperties 72 | { 73 | RedirectUri = Url.Action(nameof(Callback)), 74 | Items = 75 | { 76 | { "returnUrl", returnUrl }, 77 | { "scheme", provider }, 78 | } 79 | }; 80 | 81 | return Challenge(props, provider); 82 | } 83 | } 84 | 85 | /// 86 | /// Post processing of external authentication 87 | /// 88 | [HttpGet] 89 | public async Task Callback() 90 | { 91 | // read external identity from the temporary cookie 92 | var result = await HttpContext.AuthenticateAsync(IdentityConstants.ExternalScheme); 93 | if (result?.Succeeded != true) 94 | { 95 | throw new Exception("External authentication error"); 96 | } 97 | 98 | if (_logger.IsEnabled(LogLevel.Debug)) 99 | { 100 | var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}"); 101 | _logger.LogDebug("External claims: {@claims}", externalClaims); 102 | } 103 | 104 | // lookup our user and external provider info 105 | var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result); 106 | if (user == null) 107 | { 108 | // this might be where you might initiate a custom workflow for user registration 109 | // in this sample we don't show how that would be done, as our sample implementation 110 | // simply auto-provisions new external user 111 | user = await AutoProvisionUserAsync(provider, providerUserId, claims); 112 | } 113 | 114 | // this allows us to collect any additonal claims or properties 115 | // for the specific prtotocols used and store them in the local auth cookie. 116 | // this is typically used to store data needed for signout from those protocols. 117 | var additionalLocalClaims = new List(); 118 | var localSignInProps = new AuthenticationProperties(); 119 | ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps); 120 | //ProcessLoginCallbackForWsFed(result, additionalLocalClaims, localSignInProps); 121 | //ProcessLoginCallbackForSaml2p(result, additionalLocalClaims, localSignInProps); 122 | 123 | // issue authentication cookie for user 124 | // we must issue the cookie maually, and can't use the SignInManager because 125 | // it doesn't expose an API to issue additional claims from the login workflow 126 | var principal = await _signInManager.CreateUserPrincipalAsync(user); 127 | additionalLocalClaims.AddRange(principal.Claims); 128 | var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id; 129 | 130 | var isuser = new IdentityServerUser(user.Id) 131 | { 132 | DisplayName = name, 133 | IdentityProvider = provider, 134 | AdditionalClaims = additionalLocalClaims 135 | }; 136 | 137 | await HttpContext.SignInAsync(isuser, localSignInProps); 138 | 139 | // delete temporary cookie used during external authentication 140 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 141 | 142 | // retrieve return URL 143 | var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; 144 | 145 | // check if external login is in the context of an OIDC request 146 | var context = await _interaction.GetAuthorizationContextAsync(returnUrl); 147 | await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, name, true, context?.ClientId)); 148 | 149 | if (context != null) 150 | { 151 | if (await _clientStore.IsPkceClientAsync(context.ClientId)) 152 | { 153 | // if the client is PKCE then we assume it's native, so this change in how to 154 | // return the response is for better UX for the end user. 155 | return this.LoadingPage("Redirect", returnUrl); 156 | } 157 | } 158 | 159 | return Redirect(returnUrl); 160 | } 161 | 162 | private async Task ProcessWindowsLoginAsync(string returnUrl) 163 | { 164 | // see if windows auth has already been requested and succeeded 165 | var result = await HttpContext.AuthenticateAsync(AccountOptions.WindowsAuthenticationSchemeName); 166 | if (result?.Principal is WindowsPrincipal wp) 167 | { 168 | // we will issue the external cookie and then redirect the 169 | // user back to the external callback, in essence, treating windows 170 | // auth the same as any other external authentication mechanism 171 | var props = new AuthenticationProperties() 172 | { 173 | RedirectUri = Url.Action("Callback"), 174 | Items = 175 | { 176 | { "returnUrl", returnUrl }, 177 | { "scheme", AccountOptions.WindowsAuthenticationSchemeName }, 178 | } 179 | }; 180 | 181 | var id = new ClaimsIdentity(AccountOptions.WindowsAuthenticationSchemeName); 182 | id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value)); 183 | id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name)); 184 | 185 | // add the groups as claims -- be careful if the number of groups is too large 186 | if (AccountOptions.IncludeWindowsGroups) 187 | { 188 | var wi = wp.Identity as WindowsIdentity; 189 | var groups = wi.Groups.Translate(typeof(NTAccount)); 190 | var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value)); 191 | id.AddClaims(roles); 192 | } 193 | 194 | await HttpContext.SignInAsync( 195 | IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme, 196 | new ClaimsPrincipal(id), 197 | props); 198 | return Redirect(props.RedirectUri); 199 | } 200 | else 201 | { 202 | // trigger windows auth 203 | // since windows auth don't support the redirect uri, 204 | // this URL is re-triggered when we call challenge 205 | return Challenge(AccountOptions.WindowsAuthenticationSchemeName); 206 | } 207 | } 208 | 209 | private async Task<(ApplicationUser user, string provider, string providerUserId, IEnumerable claims)> 210 | FindUserFromExternalProviderAsync(AuthenticateResult result) 211 | { 212 | var externalUser = result.Principal; 213 | 214 | // try to determine the unique id of the external user (issued by the provider) 215 | // the most common claim type for that are the sub claim and the NameIdentifier 216 | // depending on the external provider, some other claim type might be used 217 | var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ?? 218 | externalUser.FindFirst(ClaimTypes.NameIdentifier) ?? 219 | throw new Exception("Unknown userid"); 220 | 221 | // remove the user id claim so we don't include it as an extra claim if/when we provision the user 222 | var claims = externalUser.Claims.ToList(); 223 | claims.Remove(userIdClaim); 224 | 225 | var provider = result.Properties.Items["scheme"]; 226 | var providerUserId = userIdClaim.Value; 227 | 228 | // find external user 229 | var user = await _userManager.FindByLoginAsync(provider, providerUserId); 230 | 231 | return (user, provider, providerUserId, claims); 232 | } 233 | 234 | private async Task AutoProvisionUserAsync(string provider, string providerUserId, IEnumerable claims) 235 | { 236 | // create a list of claims that we want to transfer into our store 237 | var filtered = new List(); 238 | 239 | // user's display name 240 | var name = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Name)?.Value ?? 241 | claims.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value; 242 | if (name != null) 243 | { 244 | filtered.Add(new Claim(JwtClaimTypes.Name, name)); 245 | } 246 | else 247 | { 248 | var first = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.GivenName)?.Value ?? 249 | claims.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value; 250 | var last = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.FamilyName)?.Value ?? 251 | claims.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value; 252 | if (first != null && last != null) 253 | { 254 | filtered.Add(new Claim(JwtClaimTypes.Name, first + " " + last)); 255 | } 256 | else if (first != null) 257 | { 258 | filtered.Add(new Claim(JwtClaimTypes.Name, first)); 259 | } 260 | else if (last != null) 261 | { 262 | filtered.Add(new Claim(JwtClaimTypes.Name, last)); 263 | } 264 | } 265 | 266 | // email 267 | var email = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Email)?.Value ?? 268 | claims.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value; 269 | if (email != null) 270 | { 271 | filtered.Add(new Claim(JwtClaimTypes.Email, email)); 272 | } 273 | 274 | var user = new ApplicationUser 275 | { 276 | UserName = Guid.NewGuid().ToString(), 277 | }; 278 | var identityResult = await _userManager.CreateAsync(user); 279 | if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description); 280 | 281 | if (filtered.Any()) 282 | { 283 | identityResult = await _userManager.AddClaimsAsync(user, filtered); 284 | if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description); 285 | } 286 | 287 | identityResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, providerUserId, provider)); 288 | if (!identityResult.Succeeded) throw new Exception(identityResult.Errors.First().Description); 289 | 290 | return user; 291 | } 292 | 293 | 294 | private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 295 | { 296 | // if the external system sent a session id claim, copy it over 297 | // so we can use it for single sign-out 298 | var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId); 299 | if (sid != null) 300 | { 301 | localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value)); 302 | } 303 | 304 | // if the external provider issued an id_token, we'll keep it for signout 305 | var id_token = externalResult.Properties.GetTokenValue("id_token"); 306 | if (id_token != null) 307 | { 308 | localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } }); 309 | } 310 | } 311 | 312 | //private void ProcessLoginCallbackForWsFed(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 313 | //{ 314 | //} 315 | 316 | //private void ProcessLoginCallbackForSaml2p(AuthenticateResult externalResult, List localClaims, AuthenticationProperties localSignInProps) 317 | //{ 318 | //} 319 | } 320 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Account/ExternalProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ExternalProvider 8 | { 9 | public string DisplayName { get; set; } 10 | public string AuthenticationScheme { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Account/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class LoggedOutViewModel 8 | { 9 | public string PostLogoutRedirectUri { get; set; } 10 | public string ClientName { get; set; } 11 | public string SignOutIframeUrl { get; set; } 12 | 13 | public bool AutomaticRedirectAfterSignOut { get; set; } 14 | 15 | public string LogoutId { get; set; } 16 | public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; 17 | public string ExternalAuthenticationScheme { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Account/LoginInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class LoginInputModel 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | [Required] 14 | public string Password { get; set; } 15 | public bool RememberLogin { get; set; } 16 | public string ReturnUrl { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace IdentityServer4.Quickstart.UI 10 | { 11 | public class LoginViewModel : LoginInputModel 12 | { 13 | public bool AllowRememberLogin { get; set; } = 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 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Account/LogoutInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class LogoutInputModel 8 | { 9 | public string LogoutId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Account/LogoutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class LogoutViewModel : LogoutInputModel 8 | { 9 | public bool ShowLogoutPrompt { get; set; } = true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /IdentityProvider/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 IdentityServer4.Quickstart.UI 7 | { 8 | public class RedirectViewModel 9 | { 10 | public string RedirectUrl { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityProvider/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 IdentityServer4.Quickstart.UI 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 this.LoadingPage("Redirect", 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 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Consent/ConsentInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class ConsentInputModel 10 | { 11 | public string Button { get; set; } 12 | public IEnumerable ScopesConsented { get; set; } 13 | public bool RememberConsent { get; set; } 14 | public string ReturnUrl { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 14 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Consent/ConsentViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class ConsentViewModel : ConsentInputModel 10 | { 11 | public string ClientName { get; set; } 12 | public string ClientUrl { get; set; } 13 | public string ClientLogoUrl { get; set; } 14 | public bool AllowRememberConsent { get; set; } 15 | 16 | public IEnumerable IdentityScopes { get; set; } 17 | public IEnumerable ResourceScopes { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Consent/ProcessConsentResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ProcessConsentResult 8 | { 9 | public bool IsRedirect => RedirectUri != null; 10 | public string RedirectUri { get; set; } 11 | 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 | -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Consent/ScopeViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ScopeViewModel 8 | { 9 | public string Name { get; set; } 10 | public string DisplayName { get; set; } 11 | public string Description { get; set; } 12 | public bool Emphasize { get; set; } 13 | public bool Required { get; set; } 14 | public bool Checked { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /IdentityProvider/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 IdentityServer4.Quickstart.UI.Device 6 | { 7 | public class DeviceAuthorizationInputModel : ConsentInputModel 8 | { 9 | public string UserCode { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /IdentityProvider/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 IdentityServer4.Quickstart.UI.Device 6 | { 7 | public class DeviceAuthorizationViewModel : ConsentViewModel 8 | { 9 | public string UserCode { get; set; } 10 | public bool ConfirmUserCode { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityProvider/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.Configuration; 9 | using IdentityServer4.Events; 10 | using IdentityServer4.Extensions; 11 | using IdentityServer4.Models; 12 | using IdentityServer4.Services; 13 | using IdentityServer4.Stores; 14 | using Microsoft.AspNetCore.Authorization; 15 | using Microsoft.AspNetCore.Mvc; 16 | using Microsoft.Extensions.Logging; 17 | using Microsoft.Extensions.Options; 18 | 19 | namespace IdentityServer4.Quickstart.UI.Device 20 | { 21 | [Authorize] 22 | [SecurityHeaders] 23 | public class DeviceController : Controller 24 | { 25 | private readonly IDeviceFlowInteractionService _interaction; 26 | private readonly IClientStore _clientStore; 27 | private readonly IResourceStore _resourceStore; 28 | private readonly IEventService _events; 29 | private readonly IOptions _options; 30 | private readonly ILogger _logger; 31 | 32 | public DeviceController( 33 | IDeviceFlowInteractionService interaction, 34 | IClientStore clientStore, 35 | IResourceStore resourceStore, 36 | IEventService eventService, 37 | IOptions options, 38 | ILogger logger) 39 | { 40 | _interaction = interaction; 41 | _clientStore = clientStore; 42 | _resourceStore = resourceStore; 43 | _events = eventService; 44 | _options = options; 45 | _logger = logger; 46 | } 47 | 48 | [HttpGet] 49 | public async Task Index() 50 | { 51 | string userCodeParamName = _options.Value.UserInteraction.DeviceVerificationUserCodeParameter; 52 | string userCode = Request.Query[userCodeParamName]; 53 | if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture"); 54 | 55 | var vm = await BuildViewModelAsync(userCode); 56 | if (vm == null) return View("Error"); 57 | 58 | vm.ConfirmUserCode = true; 59 | return View("UserCodeConfirmation", vm); 60 | } 61 | 62 | [HttpPost] 63 | [ValidateAntiForgeryToken] 64 | public async Task UserCodeCapture(string userCode) 65 | { 66 | var vm = await BuildViewModelAsync(userCode); 67 | if (vm == null) return View("Error"); 68 | 69 | return View("UserCodeConfirmation", vm); 70 | } 71 | 72 | [HttpPost] 73 | [ValidateAntiForgeryToken] 74 | public async Task Callback(DeviceAuthorizationInputModel model) 75 | { 76 | if (model == null) throw new ArgumentNullException(nameof(model)); 77 | 78 | var result = await ProcessConsent(model); 79 | if (result.HasValidationError) return View("Error"); 80 | 81 | return View("Success"); 82 | } 83 | 84 | private async Task ProcessConsent(DeviceAuthorizationInputModel model) 85 | { 86 | var result = new ProcessConsentResult(); 87 | 88 | var request = await _interaction.GetAuthorizationContextAsync(model.UserCode); 89 | if (request == null) return result; 90 | 91 | ConsentResponse grantedConsent = null; 92 | 93 | // user clicked 'no' - send back the standard 'access_denied' response 94 | if (model.Button == "no") 95 | { 96 | grantedConsent = ConsentResponse.Denied; 97 | 98 | // emit event 99 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested)); 100 | } 101 | // user clicked 'yes' - validate the data 102 | else if (model.Button == "yes") 103 | { 104 | // if the user consented to some scope, build the response model 105 | if (model.ScopesConsented != null && model.ScopesConsented.Any()) 106 | { 107 | var scopes = model.ScopesConsented; 108 | if (ConsentOptions.EnableOfflineAccess == false) 109 | { 110 | scopes = scopes.Where(x => x != IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess); 111 | } 112 | 113 | grantedConsent = new ConsentResponse 114 | { 115 | RememberConsent = model.RememberConsent, 116 | ScopesConsented = scopes.ToArray() 117 | }; 118 | 119 | // emit event 120 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent)); 121 | } 122 | else 123 | { 124 | result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; 125 | } 126 | } 127 | else 128 | { 129 | result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; 130 | } 131 | 132 | if (grantedConsent != null) 133 | { 134 | // communicate outcome of consent back to identityserver 135 | await _interaction.HandleRequestAsync(model.UserCode, grantedConsent); 136 | 137 | // indicate that's it ok to redirect back to authorization endpoint 138 | result.RedirectUri = model.ReturnUrl; 139 | result.ClientId = request.ClientId; 140 | } 141 | else 142 | { 143 | // we need to redisplay the consent UI 144 | result.ViewModel = await BuildViewModelAsync(model.UserCode, model); 145 | } 146 | 147 | return result; 148 | } 149 | 150 | private async Task BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null) 151 | { 152 | var request = await _interaction.GetAuthorizationContextAsync(userCode); 153 | if (request != null) 154 | { 155 | var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); 156 | if (client != null) 157 | { 158 | var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); 159 | if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) 160 | { 161 | return CreateConsentViewModel(userCode, model, client, resources); 162 | } 163 | else 164 | { 165 | _logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y)); 166 | } 167 | } 168 | else 169 | { 170 | _logger.LogError("Invalid client id: {0}", request.ClientId); 171 | } 172 | } 173 | 174 | return null; 175 | } 176 | 177 | private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, Client client, Resources resources) 178 | { 179 | var vm = new DeviceAuthorizationViewModel 180 | { 181 | UserCode = userCode, 182 | 183 | RememberConsent = model?.RememberConsent ?? true, 184 | ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), 185 | 186 | ClientName = client.ClientName ?? client.ClientId, 187 | ClientUrl = client.ClientUri, 188 | ClientLogoUrl = client.LogoUri, 189 | AllowRememberConsent = client.AllowRememberConsent 190 | }; 191 | 192 | vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 193 | vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 194 | if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) 195 | { 196 | vm.ResourceScopes = vm.ResourceScopes.Union(new[] 197 | { 198 | GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) 199 | }); 200 | } 201 | 202 | return vm; 203 | } 204 | 205 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 206 | { 207 | return new ScopeViewModel 208 | { 209 | Name = identity.Name, 210 | DisplayName = identity.DisplayName, 211 | Description = identity.Description, 212 | Emphasize = identity.Emphasize, 213 | Required = identity.Required, 214 | Checked = check || identity.Required 215 | }; 216 | } 217 | 218 | public ScopeViewModel CreateScopeViewModel(Scope scope, bool check) 219 | { 220 | return new ScopeViewModel 221 | { 222 | Name = scope.Name, 223 | DisplayName = scope.DisplayName, 224 | Description = scope.Description, 225 | Emphasize = scope.Emphasize, 226 | Required = scope.Required, 227 | Checked = check || scope.Required 228 | }; 229 | } 230 | private ScopeViewModel GetOfflineAccessScope(bool check) 231 | { 232 | return new ScopeViewModel 233 | { 234 | Name = IdentityServer4.IdentityServerConstants.StandardScopes.OfflineAccess, 235 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 236 | Description = ConsentOptions.OfflineAccessDescription, 237 | Emphasize = true, 238 | Checked = check 239 | }; 240 | } 241 | } 242 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Diagnostics/DiagnosticsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace IdentityServer4.Quickstart.UI 12 | { 13 | [SecurityHeaders] 14 | [Authorize] 15 | public class DiagnosticsController : Controller 16 | { 17 | public async Task Index() 18 | { 19 | var localAddresses = new string[] { "127.0.0.1", "::1", HttpContext.Connection.LocalIpAddress.ToString() }; 20 | if (!localAddresses.Contains(HttpContext.Connection.RemoteIpAddress.ToString())) 21 | { 22 | return NotFound(); 23 | } 24 | 25 | var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync()); 26 | return View(model); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /IdentityProvider/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 IdentityServer4.Quickstart.UI 12 | { 13 | public class DiagnosticsViewModel 14 | { 15 | public DiagnosticsViewModel(AuthenticateResult result) 16 | { 17 | AuthenticateResult = result; 18 | 19 | if (result.Properties.Items.ContainsKey("client_list")) 20 | { 21 | var encoded = result.Properties.Items["client_list"]; 22 | var bytes = Base64Url.Decode(encoded); 23 | var value = Encoding.UTF8.GetString(bytes); 24 | 25 | Clients = JsonConvert.DeserializeObject(value); 26 | } 27 | } 28 | 29 | public AuthenticateResult AuthenticateResult { get; } 30 | public IEnumerable Clients { get; } = new List(); 31 | } 32 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using IdentityServer4.Stores; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public static class Extensions 8 | { 9 | /// 10 | /// Determines whether the client is configured to use PKCE. 11 | /// 12 | /// The store. 13 | /// The client identifier. 14 | /// 15 | public static async Task IsPkceClientAsync(this IClientStore store, string client_id) 16 | { 17 | if (!string.IsNullOrWhiteSpace(client_id)) 18 | { 19 | var client = await store.FindEnabledClientByIdAsync(client_id); 20 | return client?.RequirePkce == true; 21 | } 22 | 23 | return false; 24 | } 25 | 26 | public static IActionResult LoadingPage(this Controller controller, string viewName, string redirectUri) 27 | { 28 | controller.HttpContext.Response.StatusCode = 200; 29 | controller.HttpContext.Response.Headers["Location"] = ""; 30 | 31 | return controller.View(viewName, new RedirectViewModel { RedirectUrl = redirectUri }); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /IdentityProvider/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 IdentityServer4.Quickstart.UI 16 | { 17 | /// 18 | /// This sample controller allows a user to revoke grants given to clients 19 | /// 20 | [SecurityHeaders] 21 | [Authorize] 22 | public class GrantsController : Controller 23 | { 24 | private readonly IIdentityServerInteractionService _interaction; 25 | private readonly IClientStore _clients; 26 | private readonly IResourceStore _resources; 27 | private readonly IEventService _events; 28 | 29 | public GrantsController(IIdentityServerInteractionService interaction, 30 | IClientStore clients, 31 | IResourceStore resources, 32 | IEventService events) 33 | { 34 | _interaction = interaction; 35 | _clients = clients; 36 | _resources = resources; 37 | _events = events; 38 | } 39 | 40 | /// 41 | /// Show list of grants 42 | /// 43 | [HttpGet] 44 | public async Task Index() 45 | { 46 | return View("Index", await BuildViewModelAsync()); 47 | } 48 | 49 | /// 50 | /// Handle postback to revoke a client 51 | /// 52 | [HttpPost] 53 | [ValidateAntiForgeryToken] 54 | public async Task Revoke(string clientId) 55 | { 56 | await _interaction.RevokeUserConsentAsync(clientId); 57 | await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId)); 58 | 59 | return RedirectToAction("Index"); 60 | } 61 | 62 | private async Task BuildViewModelAsync() 63 | { 64 | var grants = await _interaction.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 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Grants/GrantsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace IdentityServer4.Quickstart.UI 9 | { 10 | public class GrantsViewModel 11 | { 12 | public IEnumerable Grants { get; set; } 13 | } 14 | 15 | public class GrantViewModel 16 | { 17 | public string ClientId { get; set; } 18 | public string ClientName { get; set; } 19 | public string ClientUrl { get; set; } 20 | public string ClientLogoUrl { get; set; } 21 | public DateTime Created { get; set; } 22 | public DateTime? Expires { get; set; } 23 | public IEnumerable IdentityGrantNames { get; set; } 24 | public IEnumerable ApiGrantNames { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Home/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class ErrorViewModel 10 | { 11 | public ErrorViewModel() 12 | { 13 | } 14 | 15 | public ErrorViewModel(string error) 16 | { 17 | Error = new ErrorMessage { Error = error }; 18 | } 19 | 20 | public ErrorMessage Error { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using System.Threading.Tasks; 12 | 13 | namespace IdentityServer4.Quickstart.UI 14 | { 15 | [SecurityHeaders] 16 | [AllowAnonymous] 17 | public class HomeController : Controller 18 | { 19 | private readonly IIdentityServerInteractionService _interaction; 20 | private readonly IWebHostEnvironment _environment; 21 | private readonly ILogger _logger; 22 | 23 | public HomeController(IIdentityServerInteractionService interaction, IWebHostEnvironment environment, ILogger logger) 24 | { 25 | _interaction = interaction; 26 | _environment = environment; 27 | _logger = logger; 28 | } 29 | 30 | public IActionResult Index() 31 | { 32 | if (_environment.IsDevelopment()) 33 | { 34 | // only show in development 35 | return View(); 36 | } 37 | 38 | _logger.LogInformation("Homepage is disabled in production. Returning 404."); 39 | return NotFound(); 40 | } 41 | 42 | /// 43 | /// Shows the error page 44 | /// 45 | public async Task Error(string errorId) 46 | { 47 | var vm = new ErrorViewModel(); 48 | 49 | // retrieve error details from identityserver 50 | var message = await _interaction.GetErrorContextAsync(errorId); 51 | if (message != null) 52 | { 53 | vm.Error = message; 54 | 55 | if (!_environment.IsDevelopment()) 56 | { 57 | // only show in development 58 | message.ErrorDescription = null; 59 | } 60 | } 61 | 62 | return View("Error", vm); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /IdentityProvider/Quickstart/SecurityHeadersAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | 8 | namespace IdentityServer4.Quickstart.UI 9 | { 10 | public class SecurityHeadersAttribute : ActionFilterAttribute 11 | { 12 | public override void OnResultExecuting(ResultExecutingContext context) 13 | { 14 | var result = context.Result; 15 | if (result is ViewResult) 16 | { 17 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 18 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) 19 | { 20 | context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); 21 | } 22 | 23 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 24 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) 25 | { 26 | context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); 27 | } 28 | 29 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 30 | var csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; 31 | // also consider adding upgrade-insecure-requests once you have HTTPS in place for production 32 | //csp += "upgrade-insecure-requests;"; 33 | // also an example if you need client images to be displayed from twitter 34 | // csp += "img-src 'self' https://pbs.twimg.com;"; 35 | 36 | // once for standards compliant browsers 37 | if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) 38 | { 39 | context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); 40 | } 41 | // and once again for IE 42 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) 43 | { 44 | context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); 45 | } 46 | 47 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 48 | var referrer_policy = "no-referrer"; 49 | if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) 50 | { 51 | context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /IdentityProvider/SeedData.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.Security.Claims; 8 | using IdentityModel; 9 | using IdentityProvider.Data; 10 | using IdentityProvider.Models; 11 | using Microsoft.AspNetCore.Identity; 12 | using Microsoft.EntityFrameworkCore; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Serilog; 15 | 16 | namespace IdentityProvider 17 | { 18 | public class SeedData 19 | { 20 | public static void EnsureSeedData(string connectionString) 21 | { 22 | var services = new ServiceCollection(); 23 | services.AddLogging(); 24 | services.AddDbContext(options => 25 | options.UseSqlite(connectionString)); 26 | 27 | services.AddIdentity() 28 | .AddEntityFrameworkStores() 29 | .AddDefaultTokenProviders(); 30 | 31 | using (var serviceProvider = services.BuildServiceProvider()) 32 | { 33 | using (var scope = serviceProvider.GetRequiredService().CreateScope()) 34 | { 35 | var context = scope.ServiceProvider.GetService(); 36 | context.Database.Migrate(); 37 | 38 | var userMgr = scope.ServiceProvider.GetRequiredService>(); 39 | var alice = userMgr.FindByNameAsync("alice").Result; 40 | if (alice == null) 41 | { 42 | alice = new ApplicationUser 43 | { 44 | UserName = "alice" 45 | }; 46 | var result = userMgr.CreateAsync(alice, "Pass123$").Result; 47 | if (!result.Succeeded) 48 | { 49 | throw new Exception(result.Errors.First().Description); 50 | } 51 | 52 | result = userMgr.AddClaimsAsync(alice, new Claim[]{ 53 | new Claim(JwtClaimTypes.Name, "Alice Smith"), 54 | new Claim(JwtClaimTypes.GivenName, "Alice"), 55 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 56 | new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), 57 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 58 | new Claim(JwtClaimTypes.WebSite, "http://alice.com"), 59 | new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) 60 | }).Result; 61 | if (!result.Succeeded) 62 | { 63 | throw new Exception(result.Errors.First().Description); 64 | } 65 | Log.Debug("alice created"); 66 | } 67 | else 68 | { 69 | Log.Debug("alice already exists"); 70 | } 71 | 72 | var bob = userMgr.FindByNameAsync("bob").Result; 73 | if (bob == null) 74 | { 75 | bob = new ApplicationUser 76 | { 77 | UserName = "bob" 78 | }; 79 | var result = userMgr.CreateAsync(bob, "Pass123$").Result; 80 | if (!result.Succeeded) 81 | { 82 | throw new Exception(result.Errors.First().Description); 83 | } 84 | 85 | result = userMgr.AddClaimsAsync(bob, new Claim[]{ 86 | new Claim(JwtClaimTypes.Name, "Bob Smith"), 87 | new Claim(JwtClaimTypes.GivenName, "Bob"), 88 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 89 | new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), 90 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 91 | new Claim(JwtClaimTypes.WebSite, "http://bob.com"), 92 | new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), 93 | new Claim("location", "somewhere") 94 | }).Result; 95 | if (!result.Succeeded) 96 | { 97 | throw new Exception(result.Errors.First().Description); 98 | } 99 | Log.Debug("bob created"); 100 | } 101 | else 102 | { 103 | Log.Debug("bob already exists"); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /IdentityProvider/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityProvider.Data; 6 | using IdentityProvider.Models; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.Identity; 10 | using Microsoft.EntityFrameworkCore; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | 15 | namespace IdentityProvider 16 | { 17 | public class Startup 18 | { 19 | public IWebHostEnvironment Environment { get; } 20 | public IConfiguration Configuration { get; } 21 | 22 | public Startup(IWebHostEnvironment environment, IConfiguration configuration) 23 | { 24 | Environment = environment; 25 | Configuration = configuration; 26 | } 27 | 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddControllersWithViews(); 31 | 32 | // configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882) 33 | services.Configure(iis => 34 | { 35 | iis.AuthenticationDisplayName = "Windows"; 36 | iis.AutomaticAuthentication = false; 37 | }); 38 | 39 | // configures IIS in-proc settings 40 | services.Configure(iis => 41 | { 42 | iis.AuthenticationDisplayName = "Windows"; 43 | iis.AutomaticAuthentication = false; 44 | }); 45 | 46 | services.AddDbContext(options => 47 | options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"))); 48 | 49 | services.AddIdentity() 50 | .AddEntityFrameworkStores() 51 | .AddDefaultTokenProviders(); 52 | 53 | var builder = services.AddIdentityServer(options => 54 | { 55 | options.Events.RaiseErrorEvents = true; 56 | options.Events.RaiseInformationEvents = true; 57 | options.Events.RaiseFailureEvents = true; 58 | options.Events.RaiseSuccessEvents = true; 59 | }) 60 | .AddInMemoryIdentityResources(Config.Ids) 61 | .AddInMemoryApiResources(Config.Apis) 62 | .AddInMemoryClients(Config.Clients) 63 | .AddAspNetIdentity(); 64 | 65 | // not recommended for production - you need to store your key material somewhere secure 66 | builder.AddDeveloperSigningCredential(); 67 | 68 | services.AddAuthentication() 69 | .AddGoogle(options => 70 | { 71 | // register your IdentityServer with Google at https://console.developers.google.com 72 | // enable the Google+ API 73 | // set the redirect URI to http://localhost:5000/signin-google 74 | options.ClientId = "copy client ID from Google here"; 75 | options.ClientSecret = "copy client secret from Google here"; 76 | }); 77 | } 78 | 79 | public void Configure(IApplicationBuilder app) 80 | { 81 | if (Environment.IsDevelopment()) 82 | { 83 | app.UseDeveloperExceptionPage(); 84 | app.UseDatabaseErrorPage(); 85 | } 86 | 87 | app.UseStaticFiles(); 88 | 89 | app.UseRouting(); 90 | app.UseIdentityServer(); 91 | app.UseAuthorization(); 92 | app.UseEndpoints(endpoints => 93 | { 94 | endpoints.MapDefaultControllerRoute(); 95 | }); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /IdentityProvider/Views/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 |  2 |
3 | 6 | 7 |

You do not have access to that resource.

8 |
-------------------------------------------------------------------------------- /IdentityProvider/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 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model LoginViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityProvider/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 |
-------------------------------------------------------------------------------- /IdentityProvider/Views/Consent/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model ConsentViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Device/Success.cshtml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Device/UserCodeCapture.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | -------------------------------------------------------------------------------- /IdentityProvider/Views/Device/UserCodeConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @model IdentityServer4.Quickstart.UI.Device.DeviceAuthorizationViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityProvider/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 | } -------------------------------------------------------------------------------- /IdentityProvider/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 |
-------------------------------------------------------------------------------- /IdentityProvider/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 | -------------------------------------------------------------------------------- /IdentityProvider/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 | -------------------------------------------------------------------------------- /IdentityProvider/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 | -------------------------------------------------------------------------------- /IdentityProvider/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 | -------------------------------------------------------------------------------- /IdentityProvider/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 |
  • -------------------------------------------------------------------------------- /IdentityProvider/Views/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /IdentityProvider/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Quickstart.UI 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /IdentityProvider/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /IdentityProvider/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Data Source=AspIdUsers.db;" 4 | } 5 | } -------------------------------------------------------------------------------- /IdentityProvider/tempkey.rsa: -------------------------------------------------------------------------------- 1 | {"KeyId":"qOyeXbneDkQCy5_UyxUvgQ","Parameters":{"D":"B7vD8nQz25+MvnKp06rhb8KQB82BcVFYgjbfg50Sq4HLXI/2pl3yk0ADS+vhXz3LZtO5Je16bh3rCil6JRSaJV6jpIN2g49zjUIRHom5FghCJspdKW9rJtgTCU3kty/kvqCfTyupBjMAKptGm45f7AS8XWa5N34QPE1hZavokb1mT+/LDNFNLzvbDs0+pYgTWZl3/RgYFAOEdrtY3UAY0TKZbb/DtD/S1Txvi3+obfCRsfem2jq8zofo6BtU0urD9xM264WkQj51UUZkNONMZWvtLcbC7rt9oDlczW9HLZMeuqj2HaOBUIxaLGYbZwnjJKjEuIYkQ6cL0mTijf1npQ==","DP":"pMjatS0+WvlOa05GM6ip/ljac8RA6Ctggl77gBApJ5F25GklQ1Aar9kKdoqLzm8lqm6Vm01llpz/EEWC6EeeADlZtS53dUnf4FclNUFmylMXOI+QSPPIFRMPESvGv14ErpxInoO/lToVcfvwCfFOFXtafSHThBl0kTs1qcMlV5k=","DQ":"kl4j9zjljYYwj/V9Giy1kGwxQJekS7a4PmF1JzOmIoRKR/vfz8IcNJlQYTQO0jPBFNU33+hf/ICyfVwy0z/Wqt2Aj3VwtWZAiO+byAWoPHzONRwG0dzeOS/xXAZfYquhnZ4NkN4RHm1SerH0Xjn97EEpu9HJSpkMgBGfCpoTfAs=","Exponent":"AQAB","InverseQ":"C0Uy95HkVlAHWjAkNkFzoDwoc5+qv7U69a8Y3nQKKCm5ImxJ2on1asFdCEuvvFyxNY+qIHTpYi4agFhB8C3lODGiLb+pxyQKznVleJpyNmSRp1/n81ZqaRVIiHcNDLIfmTe+FUmdxolSI2c5g4b8HtZtmQBfMJqhYEUFf/6vrBM=","Modulus":"xZf4ouWVMPH20VT2Jv6c9R8EujChKuUilqTZ0r9BOwf4pmhRRjj7oNg7ei3W/gVBCP6zNw4btJUGS7QtxDEG4jyVOUx0E1vU9Hv+3NpWAe5Ax0Mp/3WHdjEXl39A4Z7IdegKW+rcWwZS/6UZVW5un43ce6rUAeycVMvFnoVdFUMie5MUEv9sAQmCjOdYS5y9KhSwaV7uchmcvFnazxf0Dx2BEl2w3PBbwD+3vfELnGoV8OFk1vaBR7QuROlmx50G319p8ojmdbMYzwoah2GhKP0+3Qik4rAy9bne/8GXTtOk/cE32J1TDQ1W69k7UjkhplT31T8XB7uMhrtsbtfNgQ==","P":"4w08zvp8cCnnGCi9owg7oTO8++GKTckP2sub16ywlBb8DHypSLtgbXkRwlM+SdEh1JPcULTY8lfTSpRD2N/DEohq3Ir47+zS87OlNv+y51EFgm/eX+lbcBMj00c9GCjXL0LX0hSzV0P7KAeWzGcJkuY60cz64vD0dBUiqq90owM=","Q":"3sk/txyrGu0WaO2qAzu42YZHk/AeXce3+DWgaRNWTS0vdyUa9g1y4RkK8u2fTVyZO9IOenIAHLu1bjfCThr+GHwqJe7Xi44X+tD39BHctS5EU4+alEUKeHWAFgVcA7RHSIpWaDNVk/ubCy+vJAzkFjsrrSOgccn/I3pop+kcJCs="}} -------------------------------------------------------------------------------- /IdentityProvider/updateUI.ps1: -------------------------------------------------------------------------------- 1 | iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI.AspNetIdentity/master/getmaster.ps1')) 2 | # SIG # Begin signature block 3 | # MIIfngYJKoZIhvcNAQcCoIIfjzCCH4sCAQExDzANBglghkgBZQMEAgEFADB5Bgor 4 | # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG 5 | # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAHBqDvspBu494k 6 | # uYMNuafp3nwe8Zo7LVhBGOgneS4sdqCCDgcwggPFMIICraADAgECAhACrFwmagtA 7 | # m48LefKuRiV3MA0GCSqGSIb3DQEBBQUAMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQK 8 | # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNV 9 | # BAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwHhcNMDYxMTEw 10 | # MDAwMDAwWhcNMzExMTEwMDAwMDAwWjBsMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM 11 | # RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSswKQYDVQQD 12 | # EyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMIIBIjANBgkqhkiG 13 | # 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxszlc+b71LvlLS0ypt/lgT/JzSVJtnEqw9WU 14 | # NGeiChywX2mmQLHEt7KP0JikqUFZOtPclNY823Q4pErMTSWC90qlUxI47vNJbXGR 15 | # fmO2q6Zfw6SE+E9iUb74xezbOJLjBuUIkQzEKEFV+8taiRV+ceg1v01yCT2+OjhQ 16 | # W3cxG42zxyRFmqesbQAUWgS3uhPrUQqYQUEiTmVhh4FBUKZ5XIneGUpX1S7mXRxT 17 | # LH6YzRoGFqRoc9A0BBNcoXHTWnxV215k4TeHMFYE5RG0KYAS8Xk5iKICEXwnZreI 18 | # t3jyygqoOKsKZMK/Zl2VhMGhJR6HXRpQCyASzEG7bgtROLhLywIDAQABo2MwYTAO 19 | # BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUsT7DaQP4 20 | # v0cB1JgmGggC72NkK8MwHwYDVR0jBBgwFoAUsT7DaQP4v0cB1JgmGggC72NkK8Mw 21 | # DQYJKoZIhvcNAQEFBQADggEBABwaBpfc15yfPIhmBghXIdshR/gqZ6q/GDJ2QBBX 22 | # wYrzetkRZY41+p78RbWe2UwxS7iR6EMsjrN4ztvjU3lx1uUhlAHaVYeaJGT2imbM 23 | # 3pw3zag0sWmbI8ieeCIrcEPjVUcxYRnvWMWFL04w9qAxFiPI5+JlFjPLvxoboD34 24 | # yl6LMYtgCIktDAZcUrfE+QqY0RVfnxK+fDZjOL1EpH/kJisKxJdpDemM4sAQV7jI 25 | # dhKRVfJIadi8KgJbD0TUIDHb9LpwJl2QYJ68SxcJL7TLHkNoyQcnwdJc9+ohuWgS 26 | # nDycv578gFybY83sR6olJ2egN/MAgn1U16n46S4To3foH0owggSRMIIDeaADAgEC 27 | # AhAHsEGNpR4UjDMbvN63E4MjMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNVBAYTAlVT 28 | # MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 29 | # b20xKzApBgNVBAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0Ew 30 | # HhcNMTgwNDI3MTI0MTU5WhcNMjgwNDI3MTI0MTU5WjBaMQswCQYDVQQGEwJVUzEY 31 | # MBYGA1UEChMPLk5FVCBGb3VuZGF0aW9uMTEwLwYDVQQDEyguTkVUIEZvdW5kYXRp 32 | # b24gUHJvamVjdHMgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC 33 | # AQ8AMIIBCgKCAQEAwQqv4aI0CI20XeYqTTZmyoxsSQgcCBGQnXnufbuDLhAB6GoT 34 | # NB7HuEhNSS8ftV+6yq8GztBzYAJ0lALdBjWypMfL451/84AO5ZiZB3V7MB2uxgWo 35 | # cV1ekDduU9bm1Q48jmR4SVkLItC+oQO/FIA2SBudVZUvYKeCJS5Ri9ibV7La4oo7 36 | # BJChFiP8uR+v3OU33dgm5BBhWmth4oTyq22zCfP3NO6gBWEIPFR5S+KcefUTYmn2 37 | # o7IvhvxzJsMCrNH1bxhwOyMl+DQcdWiVPuJBKDOO/hAKIxBG4i6ryQYBaKdhDgaA 38 | # NSCik0UgZasz8Qgl8n0A73+dISPumD8L/4mdywIDAQABo4IBPzCCATswHQYDVR0O 39 | # BBYEFMtck66Im/5Db1ZQUgJtePys4bFaMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY 40 | # JhoIAu9jZCvDMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzAS 41 | # BgNVHRMBAf8ECDAGAQH/AgEAMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY 42 | # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEsGA1UdHwREMEIwQKA+oDyGOmh0dHA6 43 | # Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RD 44 | # QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v 45 | # d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwDQYJKoZIhvcNAQELBQADggEBALNGxKTz6gq6 46 | # clMF01GjC3RmJ/ZAoK1V7rwkqOkY3JDl++v1F4KrFWEzS8MbZsI/p4W31Eketazo 47 | # Nxy23RT0zDsvJrwEC3R+/MRdkB7aTecsYmMeMHgtUrl3xEO3FubnQ0kKEU/HBCTd 48 | # hR14GsQEccQQE6grFVlglrew+FzehWUu3SUQEp9t+iWpX/KfviDWx0H1azilMX15 49 | # lzJUxK7kCzmflrk5jCOCjKqhOdGJoQqstmwP+07qXO18bcCzEC908P+TYkh0z9gV 50 | # rlj7tyW9K9zPVPJZsLRaBp/QjMcH65o9Y1hD1uWtFQYmbEYkT1K9tuXHtQYx1Rpf 51 | # /dC8Nbl4iukwggWlMIIEjaADAgECAhAL5Ofkz0TFYBmonpg7pehYMA0GCSqGSIb3 52 | # DQEBCwUAMFoxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw8uTkVUIEZvdW5kYXRpb24x 53 | # MTAvBgNVBAMTKC5ORVQgRm91bmRhdGlvbiBQcm9qZWN0cyBDb2RlIFNpZ25pbmcg 54 | # Q0EwHhcNMTgwNjExMDAwMDAwWhcNMjEwNjE1MTIwMDAwWjCBoDEUMBIGA1UEBRML 55 | # NjAzIDM4OSAwNjgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw 56 | # DgYDVQQHEwdSZWRtb25kMSkwJwYDVQQKEyBJZGVudGl0eVNlcnZlciAoLk5FVCBG 57 | # b3VuZGF0aW9uKTEpMCcGA1UEAxMgSWRlbnRpdHlTZXJ2ZXIgKC5ORVQgRm91bmRh 58 | # dGlvbikwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwEhFpSIYi3hrr 59 | # v/X9BtZkzufk7puhmTCVmQAPNm2R1eZZyMPhfxm5Sh/w/42CzlCG9LFgooha69z3 60 | # uoMMOKJEEQKZ6ByIV+r81o4lrHtSFbe4VlXavjQVFaVVjPSG6vWGykfHVCAeVpjx 61 | # fVk/HH6tEX506lBiHgOrQGogoQrwdVnObc3c6RiVSIuvFeCoHvk2GgiqyzFER7iO 62 | # R1055npVSAAAdxBvPA6KREcLb/qHukYCJZX4mY/SajBXwxupSnhRDbYhb+qHpFIL 63 | # x7s/azxg7tVRpVh49oJimHA2uZ/jzh/KgUsUe9MFzT7KPduBK/pfX/fXED9Pt1NN 64 | # 48VfPSuzAgMBAAGjggIeMIICGjAfBgNVHSMEGDAWgBTLXJOuiJv+Q29WUFICbXj8 65 | # rOGxWjAdBgNVHQ4EFgQU3CQnBPLvFkovKU/is0/LgQF/49swNAYDVR0RBC0wK6Ap 66 | # BggrBgEFBQcIA6AdMBsMGVVTLVdBU0hJTkdUT04tNjAzIDM4OSAwNjgwDgYDVR0P 67 | # AQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMIGZBgNVHR8EgZEwgY4wRaBD 68 | # oEGGP2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9ORVRGb3VuZGF0aW9uUHJvamVj 69 | # dHNDb2RlU2lnbmluZ0NBLmNybDBFoEOgQYY/aHR0cDovL2NybDQuZGlnaWNlcnQu 70 | # Y29tL05FVEZvdW5kYXRpb25Qcm9qZWN0c0NvZGVTaWduaW5nQ0EuY3JsMEwGA1Ud 71 | # IARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRp 72 | # Z2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4MHYwJAYIKwYB 73 | # BQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEFBQcwAoZCaHR0 74 | # cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL05FVEZvdW5kYXRpb25Qcm9qZWN0c0Nv 75 | # ZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEB 76 | # AH0FqXX4p5RlC27jX6tcTEf2HT4mAiqosnYU/napWgQE2U9m73IZO+2MuiQWkUQi 77 | # 2PLjbQQcOwfMwkt0SDaSAlfC1zhjZkZb2NcpJRg0cUAjcDzqh6hTzXRVJPD/UrW2 78 | # a5qBhYnSQDSWbYnVwfAQFFvnQcR5i/xnoOxq7+3LIvHoJafpsxcAFS57Vdsuw91u 79 | # keB6uasOfvdd06Mpl9BLWZHpyEdnPIKMv6ALibTdw9lNzCQ+EmdT5Fwky8wHE8BH 80 | # hhAdjSuGiyd+AzR3IuL96Q41h34c7pL827atOHwkkjUx+QTVkXbYoal6wwBKhi6I 81 | # QJEhT0s/yyFFM7BrLhQpRSsxghDtMIIQ6QIBATBuMFoxCzAJBgNVBAYTAlVTMRgw 82 | # FgYDVQQKEw8uTkVUIEZvdW5kYXRpb24xMTAvBgNVBAMTKC5ORVQgRm91bmRhdGlv 83 | # biBQcm9qZWN0cyBDb2RlIFNpZ25pbmcgQ0ECEAvk5+TPRMVgGaiemDul6FgwDQYJ 84 | # YIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG 85 | # 9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIB 86 | # FTAvBgkqhkiG9w0BCQQxIgQgXugViXfpDsSmxuXmOZmlxS8caPztV818gfdU0O48 87 | # oNAwDQYJKoZIhvcNAQEBBQAEggEAD/9ysHEvC7c0xnPrziJS5wXLbO5jRzqNp6wy 88 | # 5zFtVonQGdz8Z0XUeENXVRNb/oYxjWbQwZxvuOEXO93P/oNoWB2HMAGekirvS3q0 89 | # yEmBmsPLM4LFgsIOrUYQqNR0HynWdy+g4FzHNxHOKK7rO0utnzxvNX3mADtHzJl+ 90 | # 8ajOC46BlXoubbRjahEYfXVU1PyqFzL8/ztwyUT2EHjchshtHPdJ+RaHuRFTtHmV 91 | # rLzFQvLNRaddSTxxAq0ykO81/e18O1qJnPLxdbPp5bwCxmz/DWbQui72YKuvfhtQ 92 | # gkXQiiAfVdZSnNfybvD6048glMZDWpsXmF7O6mIvCJzZCuctIqGCDskwgg7FBgor 93 | # BgEEAYI3AwMBMYIOtTCCDrEGCSqGSIb3DQEHAqCCDqIwgg6eAgEDMQ8wDQYJYIZI 94 | # AWUDBAIBBQAweAYLKoZIhvcNAQkQAQSgaQRnMGUCAQEGCWCGSAGG/WwHATAxMA0G 95 | # CWCGSAFlAwQCAQUABCDxhlzu7w+OzWAwxQVBt+Syjgs8UHM7XlIuKgqyCBtWQgIR 96 | # AI87Iuor/G0hx8xkkV6T69cYDzIwMjAwMzE2MDc1MDU2WqCCC7swggaCMIIFaqAD 97 | # AgECAhAEzT+FaK52xhuw/nFgzKdtMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYT 98 | # AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2Vy 99 | # dC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3Rh 100 | # bXBpbmcgQ0EwHhcNMTkxMDAxMDAwMDAwWhcNMzAxMDE3MDAwMDAwWjBMMQswCQYD 101 | # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJDAiBgNVBAMTG1RJTUVT 102 | # VEFNUC1TSEEyNTYtMjAxOS0xMC0xNTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 103 | # AQoCggEBAOlkNZz6qZhlZBvkF9y4KTbMZwlYhU0w4Mn/5Ts8EShQrwcx4l0JGML2 104 | # iYxpCAQj4HctnRXluOihao7/1K7Sehbv+EG1HTl1wc8vp6xFfpRtrAMBmTxiPn56 105 | # /UWXMbT6t9lCPqdVm99aT1gCqDJpIhO+i4Itxpira5u0yfJlEQx0DbLwCJZ0xOiy 106 | # SKKhFKX4+uGJcEQ7je/7pPTDub0ULOsMKCclgKsQSxYSYAtpIoxOzcbVsmVZIeB8 107 | # LBKNcA6Pisrg09ezOXdQ0EIsLnrOnGd6OHdUQP9PlQQg1OvIzocUCP4dgN3Q5yt4 108 | # 6r8fcMbuQhZTNkWbUxlJYp16ApuVFKMCAwEAAaOCAzgwggM0MA4GA1UdDwEB/wQE 109 | # AwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYD 110 | # VR0gBIIBtjCCAbIwggGhBglghkgBhv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRw 111 | # czovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBB 112 | # AG4AeQAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBh 113 | # AHQAZQAgAGMAbwBuAHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBj 114 | # AGUAIABvAGYAIAB0AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABT 115 | # ACAAYQBuAGQAIAB0AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABB 116 | # AGcAcgBlAGUAbQBlAG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBh 117 | # AGIAaQBsAGkAdAB5ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBh 118 | # AHQAZQBkACAAaABlAHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAu 119 | # MAsGCWCGSAGG/WwDFTAfBgNVHSMEGDAWgBT0tuEgHf4prtLkYaWyoiWyyBc1bjAd 120 | # BgNVHQ4EFgQUVlMPwcYHp03X2G5XcoBQTOTsnsEwcQYDVR0fBGowaDAyoDCgLoYs 121 | # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC10cy5jcmwwMqAw 122 | # oC6GLGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMuY3Js 123 | # MIGFBggrBgEFBQcBAQR5MHcwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj 124 | # ZXJ0LmNvbTBPBggrBgEFBQcwAoZDaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t 125 | # L0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRFRpbWVzdGFtcGluZ0NBLmNydDANBgkqhkiG 126 | # 9w0BAQsFAAOCAQEALoOhRAVKBOO5MlL62YHwGrv4CY0juT3YkqHmRhxKL256PGNu 127 | # NxejGr9YI7JDnJSDTjkJsCzox+HizO3LeWvO3iMBR+2VVIHggHsSsa8Chqk6c2r+ 128 | # +J/BjdEhjOQpgsOKC2AAAp0fR8SftApoU39aEKb4Iub4U5IxX9iCgy1tE0Kug8EQ 129 | # TqQk9Eec3g8icndcf0/pOZgrV5JE1+9uk9lDxwQzY1E3Vp5HBBHDo1hUIdjijlbX 130 | # ST9X/AqfI1579JSN3Z0au996KqbSRaZVDI/2TIryls+JRtwxspGQo18zMGBV9fxr 131 | # MKyh7eRHTjOeZ2ootU3C7VuXgvjLqQhsUwm09zCCBTEwggQZoAMCAQICEAqhJdbW 132 | # Mht+QeQF2jaXwhUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV 133 | # BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG 134 | # A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTE2MDEwNzEyMDAw 135 | # MFoXDTMxMDEwNzEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD 136 | # ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGln 137 | # aUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQTCCASIwDQYJKoZI 138 | # hvcNAQEBBQADggEPADCCAQoCggEBAL3QMu5LzY9/3am6gpnFOVQoV7YjSsQOB0Uz 139 | # URB90Pl9TWh+57ag9I2ziOSXv2MhkJi/E7xX08PhfgjWahQAOPcuHjvuzKb2Mln+ 140 | # X2U/4Jvr40ZHBhpVfgsnfsCi9aDg3iI/Dv9+lfvzo7oiPhisEeTwmQNtO4V8CdPu 141 | # XciaC1TjqAlxa+DPIhAPdc9xck4Krd9AOly3UeGheRTGTSQjMF287DxgaqwvB8z9 142 | # 8OpH2YhQXv1mblZhJymJhFHmgudGUP2UKiyn5HU+upgPhH+fMRTWrdXyZMt7HgXQ 143 | # hBlyF/EXBu89zdZN7wZC/aJTKk+FHcQdPK/P2qwQ9d2srOlW/5MCAwEAAaOCAc4w 144 | # ggHKMB0GA1UdDgQWBBT0tuEgHf4prtLkYaWyoiWyyBc1bjAfBgNVHSMEGDAWgBRF 145 | # 66Kv9JLLgjEtUYunpyGd823IDzASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB 146 | # /wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB5BggrBgEFBQcBAQRtMGswJAYI 147 | # KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3 148 | # aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v 149 | # dENBLmNydDCBgQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQu 150 | # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2Ny 151 | # bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBQBgNV 152 | # HSAESTBHMDgGCmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cu 153 | # ZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggEB 154 | # AHGVEulRh1Zpze/d2nyqY3qzeM8GN0CE70uEv8rPAwL9xafDDiBCLK938ysfDCFa 155 | # KrcFNB1qrpn4J6JmvwmqYN92pDqTD/iy0dh8GWLoXoIlHsS6HHssIeLWWywUNUME 156 | # aLLbdQLgcseY1jxk5R9IEBhfiThhTWJGJIdjjJFSLK8pieV4H9YLFKWA1xJHcLN1 157 | # 1ZOFk362kmf7U2GJqPVrlsD0WGkNfMgBsbkodbeZY4UijGHKeZR+WfyMD+NvtQEm 158 | # tmyl7odRIeRYYJu6DC0rbaLEfrvEJStHAgh8Sa4TtuF8QkIoxhhWz0E0tmZdtnR7 159 | # 9VYzIi8iNrJLokqV2PWmjlIxggJNMIICSQIBATCBhjByMQswCQYDVQQGEwJVUzEV 160 | # MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t 161 | # MTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5n 162 | # IENBAhAEzT+FaK52xhuw/nFgzKdtMA0GCWCGSAFlAwQCAQUAoIGYMBoGCSqGSIb3 163 | # DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjAwMzE2MDc1MDU2 164 | # WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBQDJb1QXtqWMC3CL0+gHkwovig0xTAv 165 | # BgkqhkiG9w0BCQQxIgQgBxrsssmgerYZpP0eyYcuxOh4eHuDMUQIBH+cqNVIwtAw 166 | # DQYJKoZIhvcNAQEBBQAEggEAZpqXVlphTSQvw8mnTN33A58qFl/3rxttdftvOb/b 167 | # BV6ehvbdVyK0EQHQ+BNkekbS8i5UGmQHN83h0jYHmFpQGwRhv+Ef/61aCPd74coc 168 | # TW8wDp5V1umnRAxfo3XCac0EI2kbtXMjquGLjm9prEOh3LcX+GyKN6FXof0qCZRN 169 | # i3q8Y0nCsw3OdwcKf4iam0xca+7sqPWt6QB4ePw128vAluwCDBz0FglBgxwMzG1T 170 | # Zer2yvpt4tcT8turyKBneWp+rPGvYJVi6/NPquHBf9FUZ8j8fbM4xWEpfn/NL7FK 171 | # 85ofDz4RBC4XCsTrmqBfOD8FSfVDzZbe5w8tYC1ERE5+7A== 172 | # SIG # End signature block 173 | -------------------------------------------------------------------------------- /IdentityProvider/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 | } -------------------------------------------------------------------------------- /IdentityProvider/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 | -------------------------------------------------------------------------------- /IdentityProvider/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:'';} -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/wwwroot/favicon.ico -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/wwwroot/icon.jpg -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/wwwroot/icon.png -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/js/signin-redirect.js: -------------------------------------------------------------------------------- 1 | window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); 2 | -------------------------------------------------------------------------------- /IdentityProvider/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 | -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/IdentityProvider/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /StartupSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cradle77/BlazorSecurityDemo/b5ac9821abeab6b046e99070aeea2784fbf92c5f/StartupSettings.png -------------------------------------------------------------------------------- /WeatherApi/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace WeatherApi.Controllers 10 | { 11 | [Authorize(Roles = "Admin")] 12 | [ApiController] 13 | [Route("[controller]")] 14 | public class WeatherForecastController : ControllerBase 15 | { 16 | private static readonly string[] Summaries = new[] 17 | { 18 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 19 | }; 20 | 21 | private readonly ILogger _logger; 22 | 23 | public WeatherForecastController(ILogger logger) 24 | { 25 | _logger = logger; 26 | } 27 | 28 | [HttpGet] 29 | public IEnumerable Get() 30 | { 31 | var rng = new Random(); 32 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 33 | { 34 | Date = DateTime.Now.AddDays(index), 35 | TemperatureC = rng.Next(-20, 55), 36 | Summary = Summaries[rng.Next(Summaries.Length)] 37 | }) 38 | .ToArray(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /WeatherApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace WeatherApi 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /WeatherApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "WeatherApi": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "launchUrl": "weatherforecast", 8 | "applicationUrl": "https://localhost:5002", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WeatherApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authentication.JwtBearer; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.HttpsPolicy; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.Extensions.Logging; 14 | using Microsoft.IdentityModel.Tokens; 15 | 16 | namespace WeatherApi 17 | { 18 | public class Startup 19 | { 20 | public Startup(IConfiguration configuration) 21 | { 22 | Configuration = configuration; 23 | } 24 | 25 | public IConfiguration Configuration { get; } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddControllers(); 31 | 32 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 33 | .AddJwtBearer(options => 34 | { 35 | options.Authority = "https://localhost:5000"; 36 | options.Audience = "weatherapi"; 37 | options.TokenValidationParameters = new TokenValidationParameters() 38 | { 39 | NameClaimType = "name" 40 | }; 41 | }); 42 | } 43 | 44 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 45 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 46 | { 47 | if (env.IsDevelopment()) 48 | { 49 | app.UseDeveloperExceptionPage(); 50 | } 51 | 52 | app.UseCors(config => 53 | { 54 | config.AllowAnyOrigin(); 55 | config.AllowAnyMethod(); 56 | config.AllowAnyHeader(); 57 | }); 58 | 59 | app.UseHttpsRedirection(); 60 | 61 | app.UseRouting(); 62 | 63 | app.UseAuthentication(); 64 | 65 | app.UseAuthorization(); 66 | 67 | app.UseEndpoints(endpoints => 68 | { 69 | endpoints.MapControllers(); 70 | }); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /WeatherApi/WeatherApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WeatherApi/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WeatherApi 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WeatherApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /WeatherApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Blazor and Identity Server 2 | 3 | This project demonstrates how to secure a Blazor WebAssembly application with a standalone Identity Server instance and use it to acquire a token for a protected API. 4 | 5 | ![diagram](https://miro.medium.com/max/700/1*HKIgXaVF_9U3CSvhxqSPCQ.png) 6 | 7 | Please refer to [my article on Medium](https://medium.com/@marcodesanctis2/securing-blazor-webassembly-with-identity-server-4-ee44aa1687ef) for more details. 8 | 9 | > Note: as any other Blazor WASM applications, `appsettings.json` must be placed in your `wwwroot` folder. 10 | 11 | ## How to run the demo 12 | 13 | The only requirement in order to run the demo is making sure you run all the projects. My recommendation is to set it up in the solution options: 14 | 15 | ![options](https://github.com/cradle77/BlazorSecurityDemo/blob/master/StartupSettings.png?raw=true) 16 | --------------------------------------------------------------------------------