├── .dockerignore ├── .gitattributes ├── .gitignore ├── Common ├── ClaimType.cs ├── ClaimsIdentityExtensions.cs ├── Common.csproj └── Oidc.cs ├── README.md ├── REST.http ├── WebApi ├── Controllers │ ├── HomeController.cs │ └── WeatherForecastController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WeatherForecast.cs ├── WebApi.csproj └── appsettings.json ├── WebApiBearer ├── Controllers │ ├── HomeController.cs │ └── WeatherForecastController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WeatherForecast.cs ├── WebApiBearer.csproj └── appsettings.json ├── WebApp (angular) ├── .gitignore ├── ClientApp │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── browserslist │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.e2e.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── app.routes.ts │ │ │ ├── authorization.guard.ts │ │ │ ├── auto-login │ │ │ │ ├── auto-login.component.css │ │ │ │ ├── auto-login.component.html │ │ │ │ ├── auto-login.component.spec.ts │ │ │ │ └── auto-login.component.ts │ │ │ ├── forbidden │ │ │ │ ├── forbidden.component.css │ │ │ │ ├── forbidden.component.html │ │ │ │ ├── forbidden.component.spec.ts │ │ │ │ └── forbidden.component.ts │ │ │ ├── home │ │ │ │ ├── home.component.html │ │ │ │ └── home.component.ts │ │ │ ├── nav-menu │ │ │ │ ├── nav-menu.component.css │ │ │ │ ├── nav-menu.component.html │ │ │ │ └── nav-menu.component.ts │ │ │ ├── secure │ │ │ │ ├── secure.component.css │ │ │ │ ├── secure.component.html │ │ │ │ ├── secure.component.spec.ts │ │ │ │ └── secure.component.ts │ │ │ └── unauthorized │ │ │ │ ├── unauthorized.component.css │ │ │ │ ├── unauthorized.component.html │ │ │ │ ├── unauthorized.component.spec.ts │ │ │ │ └── unauthorized.component.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── index.html │ │ ├── karma.conf.js │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.css │ │ ├── test.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.server.json │ │ ├── tsconfig.spec.json │ │ └── tslint.json │ ├── tsconfig.json │ └── tslint.json ├── Controllers │ ├── ConfigController.cs │ └── WeatherForecastController.cs ├── Model │ └── OidcModel.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ └── _ViewImports.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WeatherForecast.cs ├── WebApp (angular).csproj ├── appsettings.json └── wwwroot │ └── favicon.ico ├── WebApp (blazor-serverside) ├── App.razor ├── Pages │ ├── Index.razor │ ├── Secure.razor │ ├── Signin.cshtml │ ├── Signin.cshtml.cs │ ├── Signout.cshtml │ ├── Signout.cshtml.cs │ └── _Host.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── RedirectToSignin.cs ├── Startup.cs ├── WebApp (blazor-serverside).csproj ├── _Imports.razor └── appsettings.json ├── WebApp (html-js) ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WebApp (html-js).csproj ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── index.html │ ├── openidconnect.js │ ├── openidconnect.min.js │ ├── signin-oidc-callback.html │ └── signin-oidc.html ├── aspnetcore-keycloak.sln └── keycloak_client_setup.png /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | 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 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ -------------------------------------------------------------------------------- /Common/ClaimType.cs: -------------------------------------------------------------------------------- 1 | namespace Common 2 | { 3 | public struct ClaimType 4 | { 5 | public static string AccessToken => "access_token"; 6 | 7 | public static string RefreshToken => "refresh_token"; 8 | 9 | public static string AccessTokenExpires => "access_token_expires"; 10 | 11 | public static string RefreshTokenExpires => "refresh_token_expires"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Common/ClaimsIdentityExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Common 2 | { 3 | using System; 4 | using System.IdentityModel.Tokens.Jwt; 5 | using System.Security.Claims; 6 | 7 | public static partial class Extensions 8 | { 9 | /// 10 | /// Gets an identity claim 11 | /// 12 | /// 13 | public static Claim GetClaim(this ClaimsIdentity source, string type) 14 | { 15 | if (source == null || string.IsNullOrEmpty(type)) 16 | { 17 | return default; 18 | } 19 | 20 | var claim = source.FindFirst(type); 21 | if (claim != null) 22 | { 23 | return claim; 24 | } 25 | 26 | return default; 27 | } 28 | 29 | /// 30 | /// Adds or updates a identity claim 31 | /// 32 | /// 33 | public static string GetClaimValue(this ClaimsIdentity source, string type) 34 | { 35 | if (source == null || string.IsNullOrEmpty(type)) 36 | { 37 | return default; 38 | } 39 | 40 | var claim = source.FindFirst(type); 41 | if (claim != null) 42 | { 43 | return claim.Value; 44 | } 45 | 46 | return default; 47 | } 48 | 49 | /// 50 | /// Adds or updates a identity claim 51 | /// 52 | /// 53 | public static ClaimsIdentity SetClaim(this ClaimsIdentity source, Claim claim) 54 | { 55 | if (source == null || claim == null) 56 | { 57 | return source; 58 | } 59 | 60 | if (source.FindFirst(claim.Type) != null) 61 | { 62 | source.RemoveClaim(claim); 63 | } 64 | 65 | source.AddClaim(claim); 66 | return source; 67 | } 68 | 69 | /// 70 | /// Adds or updates a identity claim 71 | /// 72 | /// 73 | public static ClaimsIdentity SetClaimValue(this ClaimsIdentity source, string type, string value) 74 | { 75 | if (source == null || string.IsNullOrEmpty(type)) 76 | { 77 | return source; 78 | } 79 | 80 | var claim = source.FindFirst(type); 81 | if (claim != null) 82 | { 83 | source.RemoveClaim(claim); 84 | } 85 | 86 | source.AddClaim(new Claim(type, value)); 87 | return source; 88 | } 89 | 90 | /// 91 | /// Adds or updates a identity claim 92 | /// 93 | /// 94 | public static ClaimsIdentity AddOrUpdateClaim(this ClaimsIdentity source, string type, long value) 95 | { 96 | if (source == null || string.IsNullOrEmpty(type)) 97 | { 98 | return source; 99 | } 100 | 101 | var claim = source.FindFirst(type); 102 | if (claim != null) 103 | { 104 | source.RemoveClaim(claim); 105 | } 106 | 107 | source.AddClaim(new Claim(type, value.ToString())); 108 | return source; 109 | } 110 | 111 | /// 112 | /// Adds or updates a identity claim 113 | /// 114 | /// 115 | public static ClaimsIdentity AddOrUpdateClaim(this ClaimsIdentity source, string type, long? value) 116 | { 117 | if (source == null || string.IsNullOrEmpty(type)) 118 | { 119 | return source; 120 | } 121 | 122 | var claim = source.FindFirst(type); 123 | if (claim != null) 124 | { 125 | source.RemoveClaim(claim); 126 | } 127 | 128 | source.AddClaim(new Claim(type, value?.ToString())); 129 | return source; 130 | } 131 | 132 | public static ClaimsIdentity SetIdentityClaims(this ClaimsIdentity source, string accessToken, string refreshToken) 133 | { 134 | return source 135 | .SetClaimValue(ClaimType.AccessToken, accessToken) 136 | .SetClaimValue(ClaimType.RefreshToken, refreshToken) 137 | .SetClaimValue(ClaimType.AccessTokenExpires, ((DateTimeOffset)new JwtSecurityToken(accessToken).ValidTo).ToUnixTimeSeconds().ToString()) 138 | .SetClaimValue(ClaimType.RefreshTokenExpires, ((DateTimeOffset)new JwtSecurityToken(refreshToken).ValidTo).ToUnixTimeSeconds().ToString()); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Common/Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Common/Oidc.cs: -------------------------------------------------------------------------------- 1 | using IdentityModel.Client; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Security.Claims; 8 | using System.Security.Principal; 9 | using System.Threading.Tasks; 10 | 11 | namespace Common 12 | { 13 | public static class Oidc 14 | { 15 | private static OidcWellKnown WellKnownConfiguration { get; set; } 16 | 17 | private static JwtKs JwtKs { get; set; } 18 | 19 | public static async Task GetWellKnownConfigurationAsync(string authority) 20 | { 21 | if (WellKnownConfiguration == null) 22 | { 23 | var client = new HttpClient 24 | { 25 | BaseAddress = new Uri(authority.TrimEnd('/') + "/") 26 | }; 27 | client.DefaultRequestHeaders.Clear(); 28 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 29 | 30 | var response = await client.GetAsync(".well-known/openid-configuration").ConfigureAwait(false); 31 | if (response.IsSuccessStatusCode) 32 | { 33 | var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 34 | WellKnownConfiguration = JsonConvert.DeserializeObject(content); 35 | } 36 | } 37 | 38 | return WellKnownConfiguration; 39 | } 40 | 41 | public static async Task GetJwtKs(string authority) 42 | { 43 | if (JwtKs == null) 44 | { 45 | var configuration = await GetWellKnownConfigurationAsync(authority).ConfigureAwait(false); 46 | var client = new HttpClient 47 | { 48 | BaseAddress = new Uri(configuration.jwks_uri) 49 | }; 50 | client.DefaultRequestHeaders.Clear(); 51 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 52 | 53 | var response = await client.GetAsync("").ConfigureAwait(false); 54 | if (response.IsSuccessStatusCode) 55 | { 56 | var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 57 | JwtKs = JsonConvert.DeserializeObject(content); 58 | } 59 | } 60 | 61 | return JwtKs; 62 | } 63 | 64 | public static async Task UpdateTokenClaims(ClaimsIdentity identity, string authority, string clientId, string clientSecret, bool force = false) 65 | { 66 | // always refresh the token, needs refresh token to be present in identity claims 67 | // or let a timer doe this constantly? https://github.com/aspnet/AspNetCore/issues/16241 68 | var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 69 | var exp = long.Parse(identity.GetClaimValue(ClaimType.AccessTokenExpires)); 70 | Console.WriteLine($"++++++ TOKEN now: {now}, exp: {exp} ++++++++"); 71 | 72 | if (now >= exp || force) 73 | { 74 | // tokens expired https://identitymodel.readthedocs.io/en/latest/client/token.html#requesting-a-token-using-the-refresh-token-grant-type 75 | Console.WriteLine("++++++ TOKEN expired ++++++++"); 76 | var response = await new HttpClient().RequestRefreshTokenAsync(new RefreshTokenRequest 77 | { 78 | Address = (await GetWellKnownConfigurationAsync(authority).ConfigureAwait(false)).token_endpoint, 79 | ClientId = clientId, 80 | ClientSecret = clientSecret, 81 | RefreshToken = identity.GetClaimValue(ClaimType.RefreshToken) 82 | }).ConfigureAwait(false); 83 | 84 | if (!response.IsError) 85 | { 86 | Console.WriteLine($"++++++ TOKEN refreshed {response.AccessToken} ++++++++"); 87 | identity.SetIdentityClaims(response.AccessToken, response.RefreshToken); 88 | } 89 | } 90 | else 91 | { 92 | Console.WriteLine($"++++++ TOKEN expires at {exp} in {TimeSpan.FromSeconds(exp - now).Minutes} minutes ++++++++"); 93 | } 94 | 95 | return identity; 96 | } 97 | } 98 | 99 | 100 | public class OidcWellKnown 101 | { 102 | public string authorization_endpoint { get; set; } 103 | public string token_endpoint { get; set; } 104 | public List token_endpoint_auth_methods_supported { get; set; } 105 | public string jwks_uri { get; set; } 106 | public List response_modes_supported { get; set; } 107 | public List subject_types_supported { get; set; } 108 | public List id_token_signing_alg_values_supported { get; set; } 109 | public bool http_logout_supported { get; set; } 110 | public bool frontchannel_logout_supported { get; set; } 111 | public string end_session_endpoint { get; set; } 112 | public List response_types_supported { get; set; } 113 | public List scopes_supported { get; set; } 114 | public string issuer { get; set; } 115 | public List claims_supported { get; set; } 116 | public bool request_uri_parameter_supported { get; set; } 117 | public string tenant_region_scope { get; set; } 118 | public string cloud_instance_name { get; set; } 119 | public string cloud_graph_host_name { get; set; } 120 | public string msgraph_host { get; set; } 121 | public string rbac_url { get; set; } 122 | } 123 | 124 | public class JwtKey 125 | { 126 | public string kty { get; set; } 127 | public string use { get; set; } 128 | public string kid { get; set; } 129 | public string x5t { get; set; } 130 | public string n { get; set; } 131 | public string e { get; set; } 132 | public List x5c { get; set; } 133 | public string issuer { get; set; } 134 | } 135 | 136 | public class JwtKs 137 | { 138 | public List keys { get; set; } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | keycloak client setup: 2 | ![](keycloak_client_setup.png) 3 | 4 | - Client Protocol = openid-connect 5 | - Access Type = confidential 6 | - client secret will appear under 'Credentials' after saving 7 | - Valid Redirect URIs = 8 | - for ASP.Net Core Authentication Middleware https://localhost:5001/signin-oidc + https://localhost:5001/signout-callback-oidc 9 | - for custom html login https://localhost:5001/signin-oidc-callback.html 10 | - Web Origins = * (otherwise /userinfo request will have empty response) 11 | 12 | ### webapi appsettings: 13 | ``` 14 | "Oidc": { 15 | "ClientId": "naos-sample", 16 | "ClientSecret": "8c87b1b0-9b2b-4ac6-bc37-88f093c04d13", 17 | "Authority": "http://localhost:8080/auth/realms/master" 18 | } 19 | ``` 20 | 21 | ### todos: 22 | - blazor app https://auth0.com/blog/what-is-blazor-tutorial-on-building-webapp-with-authentication/#Securing-the-Application-with-Auth0 23 | - [DONE] simple html page for login https://github.com/GluuFederation/openid-implicit-client 24 | - autosetup keycloak client? with api request? 25 | - https://www.7p-group.com/blog/integrationstests-mit-testcontainers-spring-security-und-keycloak/ 26 | - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_clients_resource 27 | - https://stackoverflow.com/questions/53283281/how-to-activate-the-rest-api-of-keycloak 28 | 29 | 30 | ### docker-compose 31 | ``` 32 | version: '3.4' 33 | 34 | services: 35 | keycloak: 36 | image: jboss/keycloak 37 | depends_on: 38 | - mssql 39 | - mssqlscripts 40 | ports: 41 | - 80:8080 42 | environment: 43 | - KEYCLOAK_USER=admin 44 | - KEYCLOAK_PASSWORD=admin 45 | - DB_VENDOR=mssql 46 | - DB_USER=sa 47 | - DB_PASSWORD=Abcd1234! 48 | - DB_ADDR=mssql 49 | - DB_DATABASE=Keycloak 50 | 51 | mssql: 52 | image: mcr.microsoft.com/mssql/server 53 | environment: 54 | - ACCEPT_EULA=Y 55 | - SA_PASSWORD=Abcd1234! 56 | - MSSQL_PID=Developer 57 | ports: 58 | - 1433:1433 59 | volumes: 60 | - mssql:/var/opt/mssql 61 | 62 | mssqlscripts: 63 | image: mcr.microsoft.com/mssql-tools 64 | depends_on: 65 | - mssql 66 | command: /bin/bash -c 'until /opt/mssql-tools/bin/sqlcmd -S mssql -U sa -P "Abcd1234!" -Q "create database Keycloak"; do sleep 5; done' 67 | 68 | volumes: 69 | mssql: 70 | driver: local 71 | ``` 72 | 73 | links: 74 | - https://developer.okta.com/blog/2019/11/15/aspnet-core-3-mvc-secure-authentication -------------------------------------------------------------------------------- /REST.http: -------------------------------------------------------------------------------- 1 | @baseUrl = https://global-keycloak.azurewebsites.net/auth/realms/master/protocol/openid-connect 2 | 3 | ################################################################################### 4 | #### openid configuration 5 | GET https://global-keycloak.azurewebsites.net/auth/realms/master/.well-known/openid-configuration HTTP/1.1 6 | Content-Type: application/json 7 | 8 | ################################################################################### 9 | ### AUTH sign in 10 | # @name sign_in 11 | POST {{baseUrl}}/token HTTP/1.1 12 | Content-Type: application/x-www-form-urlencoded 13 | 14 | grant_type=password 15 | &client_id=aspnetcore-keycloak 16 | &client_secret=1beb5df9-01dd-46c3-84a8-b65eca50ad57 17 | &resource=aspnetcore-keycloak 18 | &username=guest 19 | &password=guest 20 | &scope=openid 21 | 22 | @access_token = {{sign_in.response.body.$.access_token}} 23 | @refresh_token = {{sign_in.response.body.$.refresh_token}} 24 | 25 | ################################################################################### 26 | ### AUTH get user info 27 | GET {{baseUrl}}/userinfo HTTP/1.1 28 | Authorization: Bearer {{access_token}} 29 | Content-Type: application/json 30 | 31 | ################################################################################### 32 | ### AUTH refresh token https://identitymodel.readthedocs.io/en/latest/client/token.html#requesting-a-token-using-the-refresh-token-grant-type 33 | POST {{baseUrl}}/token HTTP/1.1 34 | Content-Type: application/x-www-form-urlencoded 35 | 36 | grant_type=refresh_token 37 | &client_id=aspnetcore-keycloak 38 | &client_secret=1beb5df9-01dd-46c3-84a8-b65eca50ad57 39 | &resource=aspnetcore-keycloak 40 | &refresh_token={{refresh_token}} 41 | 42 | ################################################################################### 43 | ### API get values (BEARER) 44 | GET https://localhost:5001/api/values HTTP/1.1 45 | Authorization: Bearer {{access_token}} 46 | Content-Type: application/json 47 | 48 | ################################################################################### 49 | ### API get root (BEARER) 50 | GET https://localhost:5001 HTTP/1.1 51 | Authorization: Bearer {{access_token}} 52 | Content-Type: application/json -------------------------------------------------------------------------------- /WebApi/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authentication; 5 | using Microsoft.AspNetCore.Authentication.Cookies; 6 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Configuration; 9 | 10 | namespace KeyCloak.Controllers 11 | { 12 | [Route("/")] 13 | public class HomeController : ControllerBase 14 | { 15 | private readonly IConfiguration configuration; 16 | 17 | public HomeController(IConfiguration configuration) 18 | { 19 | this.configuration = configuration; 20 | } 21 | 22 | [HttpGet] 23 | public async Task> Get() 24 | { 25 | return new string[] 26 | { 27 | $"{this.configuration["Oidc:Authority"]}/.well-known/openid-configuration", 28 | $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/api/values", 29 | $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/signin-oidc", 30 | $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/signout-oidc", 31 | "==== IDENTITY ===============", 32 | HttpContext.User?.Identity?.Name, 33 | //"==== CLAIMS ===============", 34 | HttpContext.User?.Claims?.Any() == true ? HttpContext.User?.Claims?.Select(h => $"CLAIM {h.Type}: {h.Value}").Aggregate((i, j) => i + " | " + j) : null, 35 | "==== TOKENS ===============", 36 | HttpContext.User?.Identity?.IsAuthenticated == true ? "access_token: " + await this.HttpContext.GetTokenAsync("access_token") : null, // https://www.jerriepelser.com/blog/accessing-tokens-aspnet-core-2/ 37 | HttpContext.User?.Identity?.IsAuthenticated == true ? "id_token: " + await this.HttpContext.GetTokenAsync("id_token") : null, 38 | HttpContext.User?.Identity?.IsAuthenticated == true ? "refresh_token: " + await this.HttpContext.GetTokenAsync("refresh_token") : null, 39 | "==== HEADERS ===============", 40 | HttpContext.Request.Headers.Select(h => $"HEADER {h.Key}: {h.Value}").Aggregate((i, j) => i + " | " + j) 41 | //HttpContext.Items["username"] as string 42 | }; 43 | } 44 | 45 | [Route("signin-oidc")] 46 | [HttpGet] 47 | public IActionResult Signin() 48 | { 49 | if (!HttpContext.User.Identity.IsAuthenticated) 50 | { 51 | return Challenge(OpenIdConnectDefaults.AuthenticationScheme); 52 | } 53 | 54 | return new ObjectResult(HttpContext.User.Identity); 55 | } 56 | 57 | [Route("signout-oidc")] 58 | [HttpGet] 59 | public IActionResult Signout() 60 | { 61 | var result = new SignOutResult(new[] 62 | { 63 | OpenIdConnectDefaults.AuthenticationScheme, 64 | CookieAuthenticationDefaults.AuthenticationScheme, 65 | }); 66 | 67 | result.Properties = new AuthenticationProperties(); 68 | result.Properties.RedirectUri = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/"; 69 | return result; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /WebApi/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace KeyCloak3.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/values")] 12 | public class WeatherForecastController : ControllerBase 13 | { 14 | private static readonly string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | private readonly ILogger _logger; 20 | 21 | public WeatherForecastController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | [Authorize] 27 | //[Authorize(Roles = "admin")] 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 | -------------------------------------------------------------------------------- /WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace KeyCloak3 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "WebApi": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /WebApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.Cookies; 2 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 9 | using Microsoft.IdentityModel.Tokens; 10 | 11 | namespace KeyCloak3 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.AddControllers() 25 | .AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); 26 | 27 | services.AddAuthentication(options => 28 | { 29 | options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 30 | options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 31 | }) 32 | .AddCookie() 33 | .AddOpenIdConnect(options => 34 | { 35 | options.Authority = Configuration["Oidc:Authority"]; 36 | options.ClientId = Configuration["Oidc:ClientId"]; 37 | options.ClientSecret = Configuration["Oidc:ClientSecret"]; 38 | options.SaveTokens = true; 39 | options.ResponseType = OpenIdConnectResponseType.Code; //Configuration["Oidc:ResponseType"]; 40 | options.RequireHttpsMetadata = false; // dev only 41 | options.GetClaimsFromUserInfoEndpoint = true; 42 | options.Scope.Add("openid"); 43 | options.Scope.Add("profile"); 44 | options.Scope.Add("email"); 45 | options.Scope.Add("claims"); 46 | options.SaveTokens = true; 47 | //options.Events = new OpenIdConnectEvents 48 | //{ 49 | // OnTokenResponseReceived = async ctx => 50 | // { 51 | // var a = ctx.Principal; 52 | // }, 53 | // OnAuthorizationCodeReceived = async ctx => 54 | // { 55 | // var a = ctx.Principal; 56 | // } 57 | //}; 58 | 59 | options.TokenValidationParameters = new TokenValidationParameters 60 | { 61 | NameClaimType = "name", 62 | RoleClaimType = "groups", 63 | ValidateIssuer = true 64 | }; 65 | }); 66 | 67 | // access token: http://localhost:8080/auth/realms/master/protocol/openid-connect/auth?response_type=token&client_id=naos-sample&redirect_uri=https://localhost:5001/signin-oidc 68 | 69 | services.AddAuthorization(); 70 | } 71 | 72 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 73 | { 74 | if (env.IsDevelopment()) 75 | { 76 | app.UseDeveloperExceptionPage(); 77 | } 78 | 79 | app.UseStaticFiles(); 80 | app.UseHttpsRedirection(); 81 | app.UseRouting(); 82 | app.UseAuthentication(); // added 83 | app.UseAuthorization(); 84 | app.UseEndpoints(endpoints => 85 | { 86 | endpoints.MapControllers(); 87 | }); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /WebApi/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KeyCloak3 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 | -------------------------------------------------------------------------------- /WebApi/WebApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 96053394-5af6-4a8f-a786-d95a3e54860b 6 | Linux 7 | ..\docker-compose.dcproj 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Oidc": { 11 | "Authority": "https://global-keycloak.azurewebsites.net/auth/realms/master", 12 | "ClientId": "aspnetcore-keycloak", 13 | "ClientSecret": "1beb5df9-01dd-46c3-84a8-b65eca50ad57" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebApiBearer/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authentication; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Configuration; 7 | 8 | namespace KeyCloak.Controllers 9 | { 10 | [Route("/")] 11 | public class HomeController : ControllerBase 12 | { 13 | private readonly IConfiguration configuration; 14 | 15 | public HomeController(IConfiguration configuration) 16 | { 17 | this.configuration = configuration; 18 | } 19 | 20 | [HttpGet] 21 | public async Task> Get() 22 | { 23 | return new string[] 24 | { 25 | $"{this.configuration["Oidc:Authority"]}/.well-known/openid-configuration", 26 | $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/api/values", 27 | $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/signin-oidc", 28 | $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}/signout-oidc", 29 | "==== IDENTITY ===============", 30 | HttpContext.User?.Identity?.Name, 31 | //"==== CLAIMS ===============", 32 | HttpContext.User?.Claims?.Any() == true ? HttpContext.User?.Claims?.Select(h => $"CLAIM {h.Type}: {h.Value}").Aggregate((i, j) => i + " | " + j) : null, 33 | "==== TOKENS ===============", 34 | HttpContext.User?.Identity?.IsAuthenticated == true ? "access_token: " + await this.HttpContext.GetTokenAsync("access_token") : null, // https://www.jerriepelser.com/blog/accessing-tokens-aspnet-core-2/ 35 | HttpContext.User?.Identity?.IsAuthenticated == true ? "id_token: " + await this.HttpContext.GetTokenAsync("id_token") : null, 36 | HttpContext.User?.Identity?.IsAuthenticated == true ? "refresh_token: " + await this.HttpContext.GetTokenAsync("refresh_token") : null, 37 | "==== HEADERS ===============", 38 | HttpContext.Request.Headers.Select(h => $"HEADER {h.Key}: {h.Value}").Aggregate((i, j) => i + " | " + j) 39 | //HttpContext.Items["username"] as string 40 | }; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /WebApiBearer/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace KeyCloak3.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/values")] 12 | public class WeatherForecastController : ControllerBase 13 | { 14 | private static readonly string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | private readonly ILogger _logger; 20 | 21 | public WeatherForecastController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | [Authorize] 27 | //[Authorize(Roles = "admin")] 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 | -------------------------------------------------------------------------------- /WebApiBearer/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace KeyCloak3 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebApiBearer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "WebApi": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "launchUrl": "", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | }, 10 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /WebApiBearer/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.IdentityModel.Tokens; 8 | 9 | namespace KeyCloak3 10 | { 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddControllers() 23 | .AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore); 24 | 25 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 26 | .AddJwtBearer(options => 27 | { 28 | options.Authority = this.Configuration["Oidc:Authority"]; 29 | options.Audience = this.Configuration["Oidc:ClientId"]; 30 | options.IncludeErrorDetails = true; 31 | options.TokenValidationParameters = new TokenValidationParameters 32 | { 33 | ValidateAudience = false, 34 | //ValidAudiences = new[] { "master-realm", "account" }, 35 | ValidateIssuer = true, 36 | ValidIssuer = this.Configuration["Oidc:Authority"], 37 | ValidateLifetime = false 38 | }; 39 | }); 40 | 41 | services.AddAuthorization(); 42 | } 43 | 44 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 45 | { 46 | if (env.IsDevelopment()) 47 | { 48 | app.UseDeveloperExceptionPage(); 49 | } 50 | 51 | app.UseStaticFiles(); 52 | app.UseHttpsRedirection(); 53 | app.UseRouting(); 54 | app.UseAuthentication(); // added 55 | app.UseAuthorization(); 56 | app.UseEndpoints(endpoints => 57 | { 58 | endpoints.MapControllers(); 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /WebApiBearer/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace KeyCloak3 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 | -------------------------------------------------------------------------------- /WebApiBearer/WebApiBearer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 96053394-5af6-4a8f-a786-d95a3e54860b 6 | Linux 7 | ..\docker-compose.dcproj 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /WebApiBearer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Oidc": { 11 | "Authority": "https://global-keycloak.azurewebsites.net/auth/realms/master", 12 | "ClientId": "aspnetcore-keycloak", 13 | "ClientSecret": "1beb5df9-01dd-46c3-84a8-b65eca50ad57" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebApp (angular)/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | bin/ 23 | Bin/ 24 | obj/ 25 | Obj/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | *_i.c 44 | *_p.c 45 | *_i.h 46 | *.ilk 47 | *.meta 48 | *.obj 49 | *.pch 50 | *.pdb 51 | *.pgc 52 | *.pgd 53 | *.rsp 54 | *.sbr 55 | *.tlb 56 | *.tli 57 | *.tlh 58 | *.tmp 59 | *.tmp_proj 60 | *.log 61 | *.vspscc 62 | *.vssscc 63 | .builds 64 | *.pidb 65 | *.svclog 66 | *.scc 67 | 68 | # Chutzpah Test files 69 | _Chutzpah* 70 | 71 | # Visual C++ cache files 72 | ipch/ 73 | *.aps 74 | *.ncb 75 | *.opendb 76 | *.opensdf 77 | *.sdf 78 | *.cachefile 79 | 80 | # Visual Studio profiler 81 | *.psess 82 | *.vsp 83 | *.vspx 84 | *.sap 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | nCrunchTemp_* 110 | 111 | # MightyMoose 112 | *.mm.* 113 | AutoTest.Net/ 114 | 115 | # Web workbench (sass) 116 | .sass-cache/ 117 | 118 | # Installshield output folder 119 | [Ee]xpress/ 120 | 121 | # DocProject is a documentation generator add-in 122 | DocProject/buildhelp/ 123 | DocProject/Help/*.HxT 124 | DocProject/Help/*.HxC 125 | DocProject/Help/*.hhc 126 | DocProject/Help/*.hhk 127 | DocProject/Help/*.hhp 128 | DocProject/Help/Html2 129 | DocProject/Help/html 130 | 131 | # Click-Once directory 132 | publish/ 133 | 134 | # Publish Web Output 135 | *.[Pp]ublish.xml 136 | *.azurePubxml 137 | # TODO: Comment the next line if you want to checkin your web deploy settings 138 | # but database connection strings (with potential passwords) will be unencrypted 139 | *.pubxml 140 | *.publishproj 141 | 142 | # NuGet Packages 143 | *.nupkg 144 | # The packages folder can be ignored because of Package Restore 145 | **/packages/* 146 | # except build/, which is used as an MSBuild target. 147 | !**/packages/build/ 148 | # Uncomment if necessary however generally it will be regenerated when needed 149 | #!**/packages/repositories.config 150 | 151 | # Microsoft Azure Build Output 152 | csx/ 153 | *.build.csdef 154 | 155 | # Microsoft Azure Emulator 156 | ecf/ 157 | rcf/ 158 | 159 | # Microsoft Azure ApplicationInsights config file 160 | ApplicationInsights.config 161 | 162 | # Windows Store app package directory 163 | AppPackages/ 164 | BundleArtifacts/ 165 | 166 | # Visual Studio cache files 167 | # files ending in .cache can be ignored 168 | *.[Cc]ache 169 | # but keep track of directories ending in .cache 170 | !*.[Cc]ache/ 171 | 172 | # Others 173 | ClientBin/ 174 | ~$* 175 | *~ 176 | *.dbmdl 177 | *.dbproj.schemaview 178 | *.pfx 179 | *.publishsettings 180 | orleans.codegen.cs 181 | 182 | /node_modules 183 | 184 | # RIA/Silverlight projects 185 | Generated_Code/ 186 | 187 | # Backup & report files from converting an old project file 188 | # to a newer Visual Studio version. Backup files are not needed, 189 | # because we have git ;-) 190 | _UpgradeReport_Files/ 191 | Backup*/ 192 | UpgradeLog*.XML 193 | UpgradeLog*.htm 194 | 195 | # SQL Server files 196 | *.mdf 197 | *.ldf 198 | 199 | # Business Intelligence projects 200 | *.rdl.data 201 | *.bim.layout 202 | *.bim_*.settings 203 | 204 | # Microsoft Fakes 205 | FakesAssemblies/ 206 | 207 | # GhostDoc plugin setting file 208 | *.GhostDoc.xml 209 | 210 | # Node.js Tools for Visual Studio 211 | .ntvs_analysis.dat 212 | 213 | # Visual Studio 6 build log 214 | *.plg 215 | 216 | # Visual Studio 6 workspace options file 217 | *.opt 218 | 219 | # Visual Studio LightSwitch build output 220 | **/*.HTMLClient/GeneratedArtifacts 221 | **/*.DesktopClient/GeneratedArtifacts 222 | **/*.DesktopClient/ModelManifest.xml 223 | **/*.Server/GeneratedArtifacts 224 | **/*.Server/ModelManifest.xml 225 | _Pvt_Extensions 226 | 227 | # Paket dependency manager 228 | .paket/paket.exe 229 | 230 | # FAKE - F# Make 231 | .fake/ 232 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/README.md: -------------------------------------------------------------------------------- 1 | # WebApp__angular_ 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "WebApp__angular_": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "progress": false, 17 | "extractCss": true, 18 | "outputPath": "dist", 19 | "index": "src/index.html", 20 | "main": "src/main.ts", 21 | "polyfills": "src/polyfills.ts", 22 | "tsConfig": "src/tsconfig.app.json", 23 | "assets": ["src/assets"], 24 | "styles": [ 25 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 26 | "src/styles.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "aot": true, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true 47 | } 48 | } 49 | }, 50 | "serve": { 51 | "builder": "@angular-devkit/build-angular:dev-server", 52 | "options": { 53 | "browserTarget": "WebApp__angular_:build" 54 | }, 55 | "configurations": { 56 | "production": { 57 | "browserTarget": "WebApp__angular_:build:production" 58 | } 59 | } 60 | }, 61 | "extract-i18n": { 62 | "builder": "@angular-devkit/build-angular:extract-i18n", 63 | "options": { 64 | "browserTarget": "WebApp__angular_:build" 65 | } 66 | }, 67 | "test": { 68 | "builder": "@angular-devkit/build-angular:karma", 69 | "options": { 70 | "main": "src/test.ts", 71 | "polyfills": "src/polyfills.ts", 72 | "tsConfig": "src/tsconfig.spec.json", 73 | "karmaConfig": "src/karma.conf.js", 74 | "styles": ["src/styles.css"], 75 | "scripts": [], 76 | "assets": ["src/assets"] 77 | } 78 | }, 79 | "lint": { 80 | "builder": "@angular-devkit/build-angular:tslint", 81 | "options": { 82 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], 83 | "exclude": ["**/node_modules/**"] 84 | } 85 | }, 86 | "server": { 87 | "builder": "@angular-devkit/build-angular:server", 88 | "options": { 89 | "outputPath": "dist-server", 90 | "main": "src/main.ts", 91 | "tsConfig": "src/tsconfig.server.json" 92 | }, 93 | "configurations": { 94 | "dev": { 95 | "optimization": true, 96 | "outputHashing": "all", 97 | "sourceMap": false, 98 | "namedChunks": false, 99 | "extractLicenses": true, 100 | "vendorChunk": true 101 | }, 102 | "production": { 103 | "optimization": true, 104 | "outputHashing": "all", 105 | "sourceMap": false, 106 | "namedChunks": false, 107 | "extractLicenses": true, 108 | "vendorChunk": false 109 | } 110 | } 111 | } 112 | } 113 | }, 114 | "WebApp__angular_-e2e": { 115 | "root": "e2e/", 116 | "projectType": "application", 117 | "architect": { 118 | "e2e": { 119 | "builder": "@angular-devkit/build-angular:protractor", 120 | "options": { 121 | "protractorConfig": "e2e/protractor.conf.js", 122 | "devServerTarget": "WebApp__angular_:serve" 123 | } 124 | }, 125 | "lint": { 126 | "builder": "@angular-devkit/build-angular:tslint", 127 | "options": { 128 | "tsConfig": "e2e/tsconfig.e2e.json", 129 | "exclude": ["**/node_modules/**"] 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "defaultProject": "WebApp__angular_" 136 | } 137 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require("jasmine-spec-reporter"); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: ["./src/**/*.e2e-spec.ts"], 9 | capabilities: { 10 | browserName: "chrome" 11 | }, 12 | directConnect: true, 13 | baseUrl: "http://localhost:4200/", 14 | framework: "jasmine", 15 | jasmineNodeOpts: { 16 | showColors: true, 17 | defaultTimeoutInterval: 30000, 18 | print: function() {} 19 | }, 20 | onPrepare() { 21 | require("ts-node").register({ 22 | project: require("path").join(__dirname, "./tsconfig.e2e.json") 23 | }); 24 | jasmine 25 | .getEnv() 26 | .addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getMainHeading()).toEqual('Hello, world!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getMainHeading() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp__angular_", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "build:ssr": "ng run WebApp__angular_:server:dev", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "8.2.12", 16 | "@angular/common": "8.2.12", 17 | "@angular/compiler": "8.2.12", 18 | "@angular/core": "8.2.12", 19 | "@angular/forms": "8.2.12", 20 | "@angular/platform-browser": "8.2.12", 21 | "@angular/platform-browser-dynamic": "8.2.12", 22 | "@angular/platform-server": "8.2.12", 23 | "@angular/router": "8.2.12", 24 | "angular-auth-oidc-client": "10.0.8", 25 | "@nguniversal/module-map-ngfactory-loader": "8.1.1", 26 | "aspnet-prerendering": "^3.0.1", 27 | "bootstrap": "^4.3.1", 28 | "core-js": "^3.3.3", 29 | "jquery": "3.4.1", 30 | "popper.js": "^1.16.0", 31 | "rxjs": "^6.5.3", 32 | "zone.js": "0.9.1" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "^0.803.14", 36 | "@angular/cli": "8.3.14", 37 | "@angular/compiler-cli": "8.2.12", 38 | "@angular/language-service": "8.2.12", 39 | "@types/jasmine": "~3.4.4", 40 | "@types/jasminewd2": "~2.0.8", 41 | "@types/node": "~12.11.6", 42 | "codelyzer": "^5.2.0", 43 | "jasmine-core": "~3.5.0", 44 | "jasmine-spec-reporter": "~4.2.1", 45 | "karma": "^4.4.1", 46 | "karma-chrome-launcher": "~3.1.0", 47 | "karma-coverage-istanbul-reporter": "~2.1.0", 48 | "karma-jasmine": "~2.0.1", 49 | "karma-jasmine-html-reporter": "^1.4.2", 50 | "typescript": "3.5.3" 51 | }, 52 | "optionalDependencies": { 53 | "node-sass": "^4.12.0", 54 | "protractor": "~5.4.2", 55 | "ts-node": "~8.4.1", 56 | "tslint": "~5.20.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 767px) { 2 | /* On small screens, the nav menu spans the full width of the screen. Leave a space for it. */ 3 | .body-content { 4 | padding-top: 50px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthorizationResult, OidcSecurityService, AuthorizationState } from 'angular-auth-oidc-client'; 3 | import { Router } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class AppComponent { 11 | title = 'app'; 12 | 13 | constructor(public oidcSecurityService: OidcSecurityService, 14 | private router: Router 15 | ) { 16 | if (this.oidcSecurityService.moduleSetup) { 17 | this.onOidcModuleSetup(); 18 | } else { 19 | this.oidcSecurityService.onModuleSetup.subscribe(() => { 20 | this.onOidcModuleSetup(); 21 | }); 22 | } 23 | 24 | this.oidcSecurityService.onAuthorizationResult.subscribe( 25 | (authorizationResult: AuthorizationResult) => { 26 | this.onAuthorizationResultComplete(authorizationResult); 27 | }); 28 | } 29 | 30 | ngOnInit() { 31 | } 32 | 33 | ngOnDestroy(): void { 34 | } 35 | 36 | login() { 37 | console.log('start login'); 38 | this.oidcSecurityService.authorize(); 39 | } 40 | 41 | refreshSession() { 42 | console.log('start refreshSession'); 43 | this.oidcSecurityService.authorize(); 44 | } 45 | 46 | logout() { 47 | console.log('start logoff'); 48 | this.oidcSecurityService.logoff(); 49 | } 50 | 51 | private onOidcModuleSetup() { 52 | console.log('AppComponent:onAuthorizationResultComplete'); 53 | if (window.location.hash) { 54 | this.oidcSecurityService.authorizedImplicitFlowCallback(); 55 | } else { 56 | if ('/autologin' !== window.location.pathname) { 57 | this.write('redirect', window.location.pathname); 58 | } 59 | console.log('AppComponent:onModuleSetup'); 60 | } 61 | } 62 | 63 | private onAuthorizationResultComplete(authorizationResult: AuthorizationResult) { 64 | console.log('AppComponent:onAuthorizationResultComplete'); 65 | const path = this.read('redirect'); 66 | if (authorizationResult.authorizationState === AuthorizationState.authorized) { 67 | if (path) { 68 | this.router.navigate([path]); 69 | } 70 | } else { 71 | this.router.navigate(['/unauthorized']); 72 | } 73 | } 74 | 75 | private read(key: string): any { 76 | const data = localStorage.getItem(key); 77 | if (data != null) { 78 | return JSON.parse(data); 79 | } 80 | 81 | return; 82 | } 83 | 84 | private write(key: string, value: any): void { 85 | localStorage.setItem(key, JSON.stringify(value)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | import { AppComponent } from './app.component'; 8 | import { NavMenuComponent } from './nav-menu/nav-menu.component'; 9 | import { HomeComponent } from './home/home.component'; 10 | 11 | import { 12 | AuthModule, 13 | OidcSecurityService, 14 | ConfigResult, 15 | OidcConfigService, 16 | OpenIdConfiguration 17 | } from 'angular-auth-oidc-client'; 18 | 19 | import { AutoLoginComponent } from './auto-login/auto-login.component'; 20 | import { routing } from './app.routes'; 21 | import { ForbiddenComponent } from './forbidden/forbidden.component'; 22 | import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; 23 | import { SecureComponent } from './secure/secure.component'; 24 | import { AuthorizationGuard } from './authorization.guard'; 25 | import { environment } from '../environments/environment'; 26 | 27 | export function loadConfig(oidcConfigService: OidcConfigService) { 28 | console.log('APP_INITIALIZER STARTING'); 29 | // https://login.microsoftonline.com/damienbod.onmicrosoft.com/.well-known/openid-configuration 30 | // jwt keys: https://login.microsoftonline.com/common/discovery/keys 31 | // Azure AD does not support CORS, so you need to download the OIDC configuration, and use these from the application. 32 | // The jwt keys needs to be configured in the well-known-openid-configuration.json 33 | return () => oidcConfigService.load(`${window.location.origin}/api/config/configuration`); 34 | //return () => oidcConfigService.load_using_custom_stsServer('https://localhost:44347/well-known-openid-configuration.json'); 35 | } 36 | 37 | @NgModule({ 38 | declarations: [ 39 | AppComponent, 40 | NavMenuComponent, 41 | HomeComponent, 42 | AutoLoginComponent, 43 | ForbiddenComponent, 44 | UnauthorizedComponent, 45 | SecureComponent 46 | ], 47 | imports: [ 48 | BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), 49 | HttpClientModule, 50 | AuthModule.forRoot(), 51 | FormsModule, 52 | routing, 53 | ], 54 | providers: [ 55 | OidcSecurityService, 56 | OidcConfigService, 57 | { 58 | provide: APP_INITIALIZER, 59 | useFactory: loadConfig, 60 | deps: [OidcConfigService], 61 | multi: true 62 | }, 63 | AuthorizationGuard 64 | ], 65 | bootstrap: [AppComponent] 66 | }) 67 | 68 | export class AppModule { 69 | 70 | constructor( 71 | private oidcSecurityService: OidcSecurityService, 72 | private oidcConfigService: OidcConfigService, 73 | ) { 74 | this.oidcConfigService.onConfigurationLoaded.subscribe((configResult: ConfigResult) => { 75 | 76 | const config: OpenIdConfiguration = { 77 | stsServer: 'https://login.microsoftonline.com/ed48a4e2-9762-4f41-8a15-9bcd99965fab/v2.0/', 78 | redirect_url: configResult.customConfig.redirect_url, 79 | client_id: configResult.customConfig.client_id, 80 | response_type: configResult.customConfig.response_type, 81 | scope: configResult.customConfig.scope, 82 | post_logout_redirect_uri: configResult.customConfig.post_logout_redirect_uri, 83 | start_checksession: configResult.customConfig.start_checksession, 84 | silent_renew: configResult.customConfig.silent_renew, 85 | silent_renew_url: 'https://localhost:5001/silent-renew.html', 86 | use_refresh_token: true, // https://github.com/damienbod/angular-auth-oidc-client/issues/532 87 | post_login_route: '/home', 88 | forbidden_route: configResult.customConfig.forbidden_route, 89 | unauthorized_route: configResult.customConfig.unauthorized_route, 90 | log_console_warning_active: configResult.customConfig.log_console_warning_active, 91 | log_console_debug_active: configResult.customConfig.log_console_debug_active, 92 | max_id_token_iat_offset_allowed_in_seconds: configResult.customConfig.max_id_token_iat_offset_allowed_in_seconds, 93 | auto_userinfo: false, 94 | history_cleanup_off: true, 95 | iss_validation_off: true 96 | // disable_iat_offset_validation: true 97 | }; 98 | 99 | this.oidcSecurityService.setupModule(config, configResult.authWellknownEndpoints); 100 | this.oidcSecurityService.setCustomRequestParameters(configResult.customConfig.additional_login_parameters); 101 | this.oidcSecurityService.setCustomRequestParameters({ response_mode: 'fragment' } ); 102 | }); 103 | 104 | console.log('APP STARTING'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes, RouterModule } from '@angular/router'; 2 | 3 | import { ForbiddenComponent } from './forbidden/forbidden.component'; 4 | import { HomeComponent } from './home/home.component'; 5 | import { UnauthorizedComponent } from './unauthorized/unauthorized.component'; 6 | import { AutoLoginComponent } from './auto-login/auto-login.component'; 7 | import { SecureComponent } from './secure/secure.component'; 8 | import { AuthorizationGuard } from './authorization.guard'; 9 | 10 | const appRoutes: Routes = [ 11 | { path: '', component: HomeComponent, pathMatch: 'full'}, 12 | { path: 'home', component: HomeComponent, canActivate: [AuthorizationGuard] }, 13 | { path: 'autologin', component: AutoLoginComponent }, 14 | { path: 'forbidden', component: ForbiddenComponent }, 15 | { path: 'unauthorized', component: UnauthorizedComponent }, 16 | { path: 'secure', component: SecureComponent, canActivate: [AuthorizationGuard] } 17 | ]; 18 | 19 | export const routing = RouterModule.forRoot(appRoutes); 20 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/authorization.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate, CanLoad, ActivatedRouteSnapshot, RouterStateSnapshot, Route } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { tap } from 'rxjs/operators'; 5 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 6 | 7 | @Injectable() 8 | export class AuthorizationGuard implements CanActivate, CanLoad { 9 | constructor(private router: Router, private oidcSecurityService: OidcSecurityService) { } 10 | 11 | canActivate( 12 | route: ActivatedRouteSnapshot, 13 | state: RouterStateSnapshot 14 | ): Observable { 15 | return this.checkUser(); 16 | } 17 | 18 | canLoad(state: Route): Observable { 19 | return this.checkUser(); 20 | } 21 | 22 | private checkUser(): any { 23 | console.log('AuthorizationGuard, canActivate'); 24 | 25 | return this.oidcSecurityService.getIsAuthorized().pipe( 26 | tap((isAuthorized: boolean) => { 27 | console.log('AuthorizationGuard, canActivate isAuthorized: ' + isAuthorized); 28 | 29 | if (!isAuthorized) { 30 | this.router.navigate(['/unauthorized']); 31 | } 32 | }) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/auto-login/auto-login.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vip32/aspnetcore-keycloak/51c5433edefc54062e04dfd73e02ac7437baff8a/WebApp (angular)/ClientApp/src/app/auto-login/auto-login.component.css -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/auto-login/auto-login.component.html: -------------------------------------------------------------------------------- 1 |

2 | auto-login works! 3 |

4 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/auto-login/auto-login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AutoLoginComponent } from './auto-login.component'; 4 | 5 | describe('AutoLoginComponent', () => { 6 | let component: AutoLoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ AutoLoginComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AutoLoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/auto-login/auto-login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 3 | 4 | @Component({ 5 | selector: 'app-auto-component', 6 | templateUrl: './auto-login.component.html' 7 | }) 8 | 9 | export class AutoLoginComponent implements OnInit, OnDestroy { 10 | lang: any; 11 | 12 | constructor(public oidcSecurityService: OidcSecurityService 13 | ) { 14 | this.oidcSecurityService.onModuleSetup.subscribe(() => { this.onModuleSetup(); }); 15 | } 16 | 17 | ngOnInit() { 18 | if (this.oidcSecurityService.moduleSetup) { 19 | this.onModuleSetup(); 20 | } 21 | } 22 | 23 | ngOnDestroy(): void { 24 | } 25 | 26 | private onModuleSetup() { 27 | this.oidcSecurityService.authorize(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/forbidden/forbidden.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vip32/aspnetcore-keycloak/51c5433edefc54062e04dfd73e02ac7437baff8a/WebApp (angular)/ClientApp/src/app/forbidden/forbidden.component.css -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/forbidden/forbidden.component.html: -------------------------------------------------------------------------------- 1 |

2 | forbidden works! 3 |

4 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/forbidden/forbidden.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ForbiddenComponent } from './forbidden.component'; 4 | 5 | describe('ForbiddenComponent', () => { 6 | let component: ForbiddenComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ForbiddenComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ForbiddenComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/forbidden/forbidden.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-forbidden', 5 | templateUrl: './forbidden.component.html', 6 | styleUrls: ['./forbidden.component.css'] 7 | }) 8 | export class ForbiddenComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

2 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: './home.component.html', 6 | }) 7 | export class HomeComponent { 8 | } 9 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/nav-menu/nav-menu.component.css: -------------------------------------------------------------------------------- 1 | li .glyphicon { 2 | margin-right: 10px; 3 | } 4 | 5 | /* Highlighting rules for nav menu items */ 6 | li.link-active a, 7 | li.link-active a:hover, 8 | li.link-active a:focus { 9 | background-color: #4189C7; 10 | color: white; 11 | } 12 | 13 | /* Keep the nav menu independent of scrolling and on top of other items */ 14 | .main-nav { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | z-index: 1; 20 | } 21 | 22 | @media (min-width: 768px) { 23 | /* On small screens, convert the nav menu to a vertical sidebar */ 24 | .main-nav { 25 | height: 100%; 26 | width: calc(25% - 20px); 27 | } 28 | .navbar { 29 | border-radius: 0px; 30 | border-width: 0px; 31 | height: 100%; 32 | } 33 | .navbar-header { 34 | float: none; 35 | } 36 | .navbar-collapse { 37 | border-top: 1px solid #444; 38 | padding: 0px; 39 | } 40 | .navbar ul { 41 | float: none; 42 | } 43 | .navbar li { 44 | float: none; 45 | font-size: 15px; 46 | margin: 6px; 47 | } 48 | .navbar li a { 49 | padding: 10px 16px; 50 | border-radius: 4px; 51 | } 52 | .navbar a { 53 | /* If a menu item's text is too long, truncate it */ 54 | width: 100%; 55 | white-space: nowrap; 56 | overflow: hidden; 57 | text-overflow: ellipsis; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/nav-menu/nav-menu.component.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/nav-menu/nav-menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 4 | 5 | @Component({ 6 | selector: 'app-nav-menu', 7 | templateUrl: './nav-menu.component.html', 8 | styleUrls: ['./nav-menu.component.css'] 9 | }) 10 | export class NavMenuComponent { 11 | isExpanded = false; 12 | isAuthorizedSubscription: Subscription; 13 | isAuthorized: boolean; 14 | 15 | constructor(public oidcSecurityService: OidcSecurityService) { 16 | } 17 | 18 | ngOnInit() { 19 | this.isAuthorizedSubscription = this.oidcSecurityService.getIsAuthorized().subscribe( 20 | (isAuthorized: boolean) => { 21 | this.isAuthorized = isAuthorized; 22 | }); 23 | } 24 | 25 | ngOnDestroy(): void { 26 | this.isAuthorizedSubscription.unsubscribe(); 27 | } 28 | 29 | signin() { 30 | this.oidcSecurityService.authorize(); 31 | } 32 | 33 | refreshSession() { 34 | this.oidcSecurityService.authorize(); 35 | } 36 | 37 | signout() { 38 | this.oidcSecurityService.logoff(); 39 | } 40 | collapse() { 41 | this.isExpanded = false; 42 | } 43 | 44 | toggle() { 45 | this.isExpanded = !this.isExpanded; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/secure/secure.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vip32/aspnetcore-keycloak/51c5433edefc54062e04dfd73e02ac7437baff8a/WebApp (angular)/ClientApp/src/app/secure/secure.component.css -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/secure/secure.component.html: -------------------------------------------------------------------------------- 1 |

2 | secure page 3 |

4 | 5 |

6 | id_token: {{ oidcSecurityService.getIdToken() }} 7 |

8 |

9 | access_token: {{ oidcSecurityService.getToken() }} 10 |

11 |

12 | refresh_token: {{ oidcSecurityService.getRefreshToken() }} 13 |

14 | 15 | profile 16 | 17 | 28 | 29 | TODO: call secure api 30 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/secure/secure.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SecureComponent } from './secure.component'; 4 | 5 | describe('SecureComponent', () => { 6 | let component: SecureComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SecureComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SecureComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/secure/secure.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 3 | 4 | @Component({ 5 | selector: 'app-secure', 6 | templateUrl: './secure.component.html', 7 | styleUrls: ['./secure.component.css'] 8 | }) 9 | export class SecureComponent implements OnInit { 10 | 11 | constructor(public oidcSecurityService: OidcSecurityService) { } 12 | 13 | ngOnInit() { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/unauthorized/unauthorized.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vip32/aspnetcore-keycloak/51c5433edefc54062e04dfd73e02ac7437baff8a/WebApp (angular)/ClientApp/src/app/unauthorized/unauthorized.component.css -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/unauthorized/unauthorized.component.html: -------------------------------------------------------------------------------- 1 |

2 | unauthorized works! 3 |

4 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/unauthorized/unauthorized.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UnauthorizedComponent } from './unauthorized.component'; 4 | 5 | describe('UnauthorizedComponent', () => { 6 | let component: UnauthorizedComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ UnauthorizedComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UnauthorizedComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/app/unauthorized/unauthorized.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-unauthorized', 5 | templateUrl: './unauthorized.component.html', 6 | styleUrls: ['./unauthorized.component.css'] 7 | }) 8 | export class UnauthorizedComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vip32/aspnetcore-keycloak/51c5433edefc54062e04dfd73e02ac7437baff8a/WebApp (angular)/ClientApp/src/assets/.gitkeep -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dotnet_angular_google_oidc 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | export function getBaseUrl() { 8 | return document.getElementsByTagName('base')[0].href; 9 | } 10 | 11 | const providers = [ 12 | { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] } 13 | ]; 14 | 15 | if (environment.production) { 16 | enableProdMode(); 17 | } 18 | 19 | platformBrowserDynamic(providers).bootstrapModule(AppModule) 20 | .catch(err => console.log(err)); 21 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | /* Provide sufficient contrast against white background */ 4 | a { 5 | color: #0366d6; 6 | } 7 | 8 | code { 9 | color: #e01a76; 10 | } 11 | 12 | .btn-primary { 13 | color: #fff; 14 | background-color: #1b6ec2; 15 | border-color: #1861ac; 16 | } 17 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "src/test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "angularCompilerOptions": { 7 | "entryModule": "app/app.server.module#AppServerModule" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "module": "esnext", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es2015", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2017", 18 | "dom" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /WebApp (angular)/ClientApp/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "no-inputs-metadata-property": true, 121 | "no-outputs-metadata-property": true, 122 | "no-host-metadata-property": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-lifecycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /WebApp (angular)/Controllers/ConfigController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Newtonsoft.Json; 9 | using Microsoft.Extensions.Hosting; 10 | using System.Linq; 11 | 12 | namespace WebApp__angular_.Controllers 13 | { 14 | [Route("api/config")] 15 | public class ConfigController : Controller 16 | { 17 | private readonly IConfiguration configuration; 18 | private readonly IWebHostEnvironment environment; 19 | 20 | private OidcWellKnown wellKnown; 21 | private JwtKs _jwtKs; 22 | 23 | public ConfigController(IConfiguration config, IWebHostEnvironment environment) 24 | { 25 | this.configuration = config; 26 | this.environment = environment; 27 | } 28 | 29 | [HttpGet(".well-known/openid-configuration")] 30 | public async Task WellKnownAsync() 31 | { 32 | Console.WriteLine("GET .well-known/openid-configuration"); 33 | var protocol = this.Request.IsHttps ? "https://" : "http://"; 34 | var jwks_uri = $"{protocol}{this.Request.Host.ToUriComponent()}/api/config/discovery/keys"; 35 | var wellKnown = await this.GetWellKnownAsync().ConfigureAwait(false); 36 | wellKnown.jwks_uri = jwks_uri; 37 | return wellKnown; 38 | } 39 | 40 | [HttpGet("discovery/keys")] 41 | public async Task KeysAsync() 42 | { 43 | return await this.GetJwtKs().ConfigureAwait(false); 44 | } 45 | 46 | [HttpGet("profile")] 47 | public async Task Profile() 48 | { 49 | return HttpContext.User?.Claims?.Select(h => $"CLAIM {h.Type}: {h.Value}").Aggregate((i, j) => i + " | " + j); 50 | } 51 | 52 | [HttpGet("configuration")] 53 | public async Task ConfigurationAsync() 54 | { 55 | Console.WriteLine("GET configuration"); 56 | var config = new OidcConfig(); 57 | var wellKnown = await this.GetWellKnownAsync().ConfigureAwait(false); 58 | 59 | var protocol = this.Request.IsHttps ? "https://" : "http://"; 60 | config.stsServer = $"{protocol}{this.Request.Host.ToUriComponent()}/api/config"; 61 | config.redirect_url = $"{protocol}{this.Request.Host.ToUriComponent()}/"; 62 | config.client_id = this.configuration["Oidc:ClientId"]; 63 | config.response_type = "id_token token"; 64 | if (!String.IsNullOrEmpty(this.configuration["Oidc:Scope"])) 65 | { 66 | config.scope = this.configuration["Oidc:Scope"]; 67 | } 68 | else 69 | { 70 | config.scope = "openid profile email claims"; 71 | } 72 | config.post_logout_redirect_uri = $"{protocol}{this.Request.Host.ToUriComponent()}/"; 73 | config.post_login_route = "/home"; 74 | config.forbidden_route = "/home"; 75 | config.unauthorized_route = "/home"; 76 | config.auto_userinfo = false; 77 | config.log_console_warning_active = true; 78 | config.log_console_debug_active = this.environment.IsDevelopment(); 79 | config.max_id_token_iat_offset_allowed_in_seconds = 1000; 80 | if (!String.IsNullOrEmpty(this.configuration["Oidc:Resource"])) 81 | { 82 | config.additional_login_parameters["resource"] = this.configuration["Oidc:Resource"]; 83 | } 84 | if (!String.IsNullOrEmpty(this.configuration["Oidc:Prompt"])) 85 | { 86 | config.additional_login_parameters["prompt"] = this.configuration["Oidc:Prompt"]; 87 | } 88 | return config; 89 | } 90 | 91 | private async Task GetWellKnownAsync() 92 | { 93 | if (this.wellKnown == null) 94 | { 95 | var client = new HttpClient(); 96 | client.BaseAddress = new Uri(this.configuration["Oidc:Authority"] + "/"); 97 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 98 | 99 | var response = await client.GetAsync(".well-known/openid-configuration").ConfigureAwait(false); 100 | if (response.IsSuccessStatusCode) 101 | { 102 | var wellknownString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 103 | this.wellKnown = JsonConvert.DeserializeObject(wellknownString); 104 | } 105 | } 106 | return this.wellKnown; 107 | } 108 | 109 | private async Task GetJwtKs() 110 | { 111 | if (this._jwtKs == null) 112 | { 113 | var client = new HttpClient(); 114 | var wellKnown = await this.GetWellKnownAsync().ConfigureAwait(false); 115 | client.BaseAddress = new Uri(wellKnown.jwks_uri); 116 | client.DefaultRequestHeaders.Clear(); 117 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 118 | 119 | var response = await client.GetAsync("").ConfigureAwait(false); 120 | if (response.IsSuccessStatusCode) 121 | { 122 | var jwrkstring = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 123 | this._jwtKs = JsonConvert.DeserializeObject(jwrkstring); 124 | } 125 | } 126 | 127 | return this._jwtKs; 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /WebApp (angular)/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace WebApp__angular_.Controllers 9 | { 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class WeatherForecastController : ControllerBase 13 | { 14 | private static readonly string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | private readonly ILogger _logger; 20 | 21 | public WeatherForecastController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | [HttpGet] 27 | public IEnumerable Get() 28 | { 29 | var rng = new Random(); 30 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 31 | { 32 | Date = DateTime.Now.AddDays(index), 33 | TemperatureC = rng.Next(-20, 55), 34 | Summary = Summaries[rng.Next(Summaries.Length)] 35 | }) 36 | .ToArray(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WebApp (angular)/Model/OidcModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace WebApp__angular_ 4 | { 5 | public class OidcConfig 6 | { 7 | public string stsServer { get; set; } 8 | public string redirect_url { get; set; } 9 | public string client_id { get; set; } 10 | public string response_type { get; set; } 11 | public string scope { get; set; } 12 | public string post_logout_redirect_uri { get; set; } 13 | public string post_login_route { get; set; } 14 | public bool start_checksession { get; set; } 15 | public bool silent_renew { get; set; } 16 | public string silent_renew_url { get; set; } 17 | public string startup_route { get; set; } 18 | public string forbidden_route { get; set; } 19 | public string unauthorized_route { get; set; } 20 | public bool auto_userinfo { get; set; } 21 | public bool log_console_warning_active { get; set; } 22 | public bool log_console_debug_active { get; set; } 23 | public int max_id_token_iat_offset_allowed_in_seconds { get; set; } 24 | public Dictionary additional_login_parameters { get; } = new Dictionary(); 25 | } 26 | 27 | public class OidcWellKnown 28 | { 29 | public string authorization_endpoint { get; set; } 30 | public string token_endpoint { get; set; } 31 | public List token_endpoint_auth_methods_supported { get; set; } 32 | public string jwks_uri { get; set; } 33 | public List response_modes_supported { get; set; } 34 | public List subject_types_supported { get; set; } 35 | public List id_token_signing_alg_values_supported { get; set; } 36 | public bool http_logout_supported { get; set; } 37 | public bool frontchannel_logout_supported { get; set; } 38 | public string end_session_endpoint { get; set; } 39 | public List response_types_supported { get; set; } 40 | public List scopes_supported { get; set; } 41 | public string issuer { get; set; } 42 | public List claims_supported { get; set; } 43 | public bool request_uri_parameter_supported { get; set; } 44 | public string tenant_region_scope { get; set; } 45 | public string cloud_instance_name { get; set; } 46 | public string cloud_graph_host_name { get; set; } 47 | public string msgraph_host { get; set; } 48 | public string rbac_url { get; set; } 49 | } 50 | 51 | public class JwtKey 52 | { 53 | public string kty { get; set; } 54 | public string use { get; set; } 55 | public string kid { get; set; } 56 | public string x5t { get; set; } 57 | public string n { get; set; } 58 | public string e { get; set; } 59 | public List x5c { get; set; } 60 | public string issuer { get; set; } 61 | } 62 | 63 | public class JwtKs 64 | { 65 | public List keys { get; set; } 66 | } 67 | } -------------------------------------------------------------------------------- /WebApp (angular)/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /WebApp (angular)/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace WebApp__angular_.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | public class ErrorModel : PageModel 14 | { 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public string RequestId { get; set; } 23 | 24 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 25 | 26 | public void OnGet() 27 | { 28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WebApp (angular)/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using WebApp__angular_ 2 | @namespace WebApp__angular_.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /WebApp (angular)/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 WebApp__angular_ 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 | -------------------------------------------------------------------------------- /WebApp (angular)/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true 5 | }, 6 | "profiles": { 7 | "WebApp__angular_": { 8 | "commandName": "Project", 9 | "launchBrowser": true, 10 | "launchUrl": "", 11 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 12 | "environmentVariables": { 13 | "ASPNETCORE_ENVIRONMENT": "Development" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebApp (angular)/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.HttpsPolicy; 4 | using Microsoft.AspNetCore.SpaServices.AngularCli; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | namespace WebApp__angular_ 10 | { 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddControllersWithViews(); 24 | // In production, the Angular files will be served from this directory 25 | services.AddSpaStaticFiles(configuration => 26 | { 27 | configuration.RootPath = "ClientApp/dist"; 28 | }); 29 | } 30 | 31 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 32 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 33 | { 34 | if (env.IsDevelopment()) 35 | { 36 | app.UseDeveloperExceptionPage(); 37 | } 38 | else 39 | { 40 | app.UseExceptionHandler("/Error"); 41 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 42 | app.UseHsts(); 43 | } 44 | 45 | app.UseHttpsRedirection(); 46 | app.UseStaticFiles(); 47 | if (!env.IsDevelopment()) 48 | { 49 | app.UseSpaStaticFiles(); 50 | } 51 | 52 | app.UseRouting(); 53 | 54 | app.UseEndpoints(endpoints => 55 | { 56 | endpoints.MapControllerRoute( 57 | name: "default", 58 | pattern: "{controller}/{action=Index}/{id?}"); 59 | }); 60 | 61 | app.UseSpa(spa => 62 | { 63 | // To learn more about options for serving an Angular SPA from ASP.NET Core, 64 | // see https://go.microsoft.com/fwlink/?linkid=864501 65 | 66 | spa.Options.SourcePath = "ClientApp"; 67 | 68 | if (env.IsDevelopment()) 69 | { 70 | spa.UseAngularCliServer(npmScript: "start"); 71 | } 72 | }); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /WebApp (angular)/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WebApp__angular_ 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 | -------------------------------------------------------------------------------- /WebApp (angular)/WebApp (angular).csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | 11 | 12 | false 13 | WebApp__angular_ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | %(DistFiles.Identity) 49 | PreserveNewest 50 | true 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /WebApp (angular)/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Oidc": { 11 | "Authority": "https://global-keycloak.azurewebsites.net/auth/realms/master", 12 | "ClientId": "aspnetcore-keycloak", 13 | "ClientSecret": "1beb5df9-01dd-46c3-84a8-b65eca50ad57", 14 | "Resource": "aspnetcore-keycloakaaaaaaaaaa", 15 | "Scope": "openid profile email claims" 16 | } 17 | } -------------------------------------------------------------------------------- /WebApp (angular)/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vip32/aspnetcore-keycloak/51c5433edefc54062e04dfd73e02ac7437baff8a/WebApp (angular)/wwwroot/favicon.ico -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @*

401 - Sorry

7 |

40 - unauthorized

*@ 8 |
9 |
10 |
11 | 12 |

404 - Sorry

13 |

404 - page not found

14 |
15 |
16 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |

aspnetcore-keycloak (blazor)

4 | 5 | Hello @Name 6 | 7 | 8 | 9 | 10 | 11 | signin 12 | 13 | 14 | signout 15 | 16 | 17 | 18 |

19 | secure
20 |

21 | 22 | @code { 23 | public string Name { get; set; } = "foo"; 24 | 25 | protected async Task Toggle() 26 | { 27 | if (Name == "foo") 28 | { 29 | Name = "bar"; 30 | } 31 | else 32 | { 33 | Name = "foo"; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Pages/Secure.razor: -------------------------------------------------------------------------------- 1 | @page "/secure" 2 | @attribute [Authorize] 3 | @inject OpenIdConnectOptions oidcOptions 4 | @inject AuthenticationStateProvider AuthenticationStateProvider 5 | @inject IHttpContextAccessor ContextAccessor 6 | @inject IConfiguration Configuration 7 | @*@inject ClaimsPrincipal CurrentUser*@ 8 | 9 |

aspnetcore-keycloak (blazor)

10 | 11 | Secure: 12 | 13 |

14 | home
15 |

16 | 17 | 18 |

role:you are part of 'admin'

19 |
20 | 21 | 22 |

role:you are part of 'role1'

23 |
24 | 25 | 26 |
    27 |
  • 28 | ====================== HTTP ============================================= 29 |
  • 30 |
  • 31 | user: @ContextAccessor.HttpContext.User.Identity.Name 32 |
  • 33 |
  • 34 | isAuthenticated: @ContextAccessor.HttpContext.User.Identity.IsAuthenticated 35 |
  • 36 |
  • 37 | access_token: @ContextAccessor.HttpContext.GetTokenAsync("access_token").Result 38 |
  • 39 |
  • 40 | id_token: @ContextAccessor.HttpContext.GetTokenAsync("id_token").Result 41 |
  • 42 |
  • 43 | refresh_token: @ContextAccessor.HttpContext.GetTokenAsync("refresh_token").Result 44 |
  • 45 |
  • 46 | ====================== CLAIMS ============================================= 47 |
  • 48 | @foreach (var claim in ContextAccessor.HttpContext.User.Claims.OrderBy(c => c.Type)) 49 | { 50 |
  • 51 | @claim.Type : @claim.Value 52 |
  • 53 | } 54 | 55 |
56 | 57 |

61 |
62 | 63 | 64 | 65 | @code { 66 | private async Task Log() 67 | { 68 | var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); 69 | var user = authState.User; 70 | 71 | if (user.Identity.IsAuthenticated) 72 | { 73 | var user1 = ContextAccessor.HttpContext.User.Identity.Name; 74 | Console.WriteLine($"user '{user.Identity.Name}' is authenticated."); 75 | } 76 | else 77 | { 78 | Console.WriteLine("user is NOT authenticated."); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Pages/Signin.cshtml: -------------------------------------------------------------------------------- 1 | @page "/signin" 2 | @model web.Pages.SigninModel 3 | @{ 4 | } 5 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Pages/Signin.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | 6 | namespace web.Pages 7 | { 8 | public class SigninModel : PageModel 9 | { 10 | public async Task OnGet(string redirectUri) 11 | { 12 | await HttpContext.ChallengeAsync( 13 | OpenIdConnectDefaults.AuthenticationScheme, 14 | new AuthenticationProperties 15 | { 16 | RedirectUri = redirectUri, 17 | }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Pages/Signout.cshtml: -------------------------------------------------------------------------------- 1 | @page "/signout" 2 | @model web.Pages.SignoutModel 3 | @{ 4 | } 5 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Pages/Signout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | 6 | namespace web.Pages 7 | { 8 | public class SignoutModel : PageModel 9 | { 10 | public async Task OnGet() 11 | { 12 | await HttpContext.SignOutAsync(); 13 | return Redirect("/"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace web.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | 5 | 6 | 7 | 8 | blazor-minimal 9 | 10 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace web 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "WebApp": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "launchUrl": "", 8 | "environmentVariables": { 9 | "ASPNETCORE_ENVIRONMENT": "Development" 10 | }, 11 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/RedirectToSignin.cs: -------------------------------------------------------------------------------- 1 | namespace web 2 | { 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | public class RedirectToSignin : ComponentBase 7 | { 8 | [Inject] 9 | protected NavigationManager NavigationManager { get; set; } 10 | 11 | [Inject] 12 | protected IHttpContextAccessor Context { get; set; } 13 | 14 | protected override void OnInitialized() 15 | { 16 | if (!this.Context.HttpContext.User.Identity.IsAuthenticated) 17 | { 18 | var challengeUri = "/signin?redirectUri=" + System.Net.WebUtility.UrlEncode(this.NavigationManager.Uri); 19 | this.NavigationManager.NavigateTo(challengeUri, true); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/Startup.cs: -------------------------------------------------------------------------------- 1 | using IdentityModel.Client; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Authentication.Cookies; 4 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.IdentityModel.Protocols.OpenIdConnect; 11 | using Microsoft.IdentityModel.Tokens; 12 | using System; 13 | using System.Net; 14 | using System.Net.Http; 15 | using System.Security.Claims; 16 | using System.Threading.Tasks; 17 | using Common; 18 | using Microsoft.AspNetCore.Http; 19 | using System.IdentityModel.Tokens.Jwt; 20 | 21 | namespace web 22 | { 23 | public class Startup 24 | { 25 | public Startup(IConfiguration configuration) 26 | { 27 | Configuration = configuration; 28 | } 29 | 30 | public IConfiguration Configuration { get; } 31 | 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | services.AddControllers(); 35 | 36 | // added 37 | services.AddRazorPages(); 38 | services.AddServerSideBlazor(); 39 | 40 | var oidcOptions = new OpenIdConnectOptions(); 41 | this.Configuration.GetSection("Oidc").Bind(oidcOptions); 42 | services.AddSingleton(oidcOptions); 43 | 44 | services.AddAuthentication(options => 45 | { 46 | options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 47 | options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 48 | }) 49 | .AddCookie(options => 50 | { 51 | options.Cookie.SameSite = SameSiteMode.None; 52 | options.Cookie.Name = "AuthCookie"; 53 | options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; 54 | options.SlidingExpiration = true; 55 | options.Events = new CookieAuthenticationEvents 56 | { 57 | OnValidatePrincipal = async c => 58 | { 59 | // this event is fired everytime the cookie has been validated by the cookie middleware, so basically during every authenticated request. 60 | // the decryption of the cookie has already happened so we have access to the identity + user claims 61 | // and cookie properties - expiration, etc.. 62 | // source: https://github.com/mderriey/aspnet-core-token-renewal/blob/2fd9abcc2abe92df2b6c4374ad3f2ce585b6f953/src/MvcClient/Startup.cs#L57 63 | var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 64 | var exp = c.Properties.ExpiresUtc.GetValueOrDefault().ToUnixTimeSeconds(); 65 | 66 | if (now >= exp) // session cookie expired? 67 | { 68 | var response = await new HttpClient().RequestRefreshTokenAsync(new RefreshTokenRequest 69 | { 70 | Address = oidcOptions.Authority + "/protocol/openid-connect/token", // AAD="/oauth2/token", 71 | ClientId = Configuration["Oidc:ClientId"], 72 | ClientSecret = Configuration["Oidc:ClientSecret"], 73 | RefreshToken = ((ClaimsIdentity)c.Principal.Identity).GetClaimValue(ClaimType.RefreshToken) //c.Properties.Items[".Token.refresh_token"] // check if present 74 | }).ConfigureAwait(false); 75 | 76 | if (!response.IsError) 77 | { 78 | ((ClaimsIdentity)c.Principal.Identity) 79 | .SetIdentityClaims(response.AccessToken, response.RefreshToken); 80 | 81 | c.ShouldRenew = true; // renew session cookie 82 | } 83 | } 84 | } 85 | }; 86 | }) 87 | .AddOpenIdConnect(options => 88 | { 89 | options.Authority = oidcOptions.Authority; 90 | options.ClientId = oidcOptions.ClientId; 91 | options.ClientSecret = oidcOptions.ClientSecret; 92 | options.SaveTokens = true; 93 | options.ResponseType = oidcOptions.ResponseType; 94 | options.Resource = oidcOptions.Resource; // needed for proper jwt format access_token 95 | options.RequireHttpsMetadata = oidcOptions.RequireHttpsMetadata; // dev only 96 | options.GetClaimsFromUserInfoEndpoint = oidcOptions.GetClaimsFromUserInfoEndpoint; // does not work together with options.resource 97 | //options.CallbackPath = oidcOptions.CallbackPath; // "/signin-oidc/" 98 | //options.SignedOutCallbackPath = oidcOptions.SignedOutCallbackPath; // "/signout-oidc/" 99 | options.SaveTokens = oidcOptions.SaveTokens; 100 | 101 | options.Scope.Clear(); 102 | foreach (var scope in oidcOptions.Scope) 103 | { 104 | options.Scope.Add(scope); 105 | } 106 | 107 | options.Events = new OpenIdConnectEvents 108 | { 109 | OnTokenValidated = t => 110 | { 111 | // this event is called after the OIDC middleware received the auhorisation code, redeemed it for an access token + a refresh token 112 | // and validated the identity token 113 | ((ClaimsIdentity)t.Principal.Identity) 114 | .SetIdentityClaims(t.TokenEndpointResponse.AccessToken, t.TokenEndpointResponse.RefreshToken); 115 | 116 | t.Properties.ExpiresUtc = new JwtSecurityToken(t.TokenEndpointResponse.AccessToken).ValidTo; // align expiration of the cookie with expiration of the access token 117 | t.Properties.IsPersistent = true; // so that we don't issue a session cookie but one with a fixed expiration 118 | 119 | return Task.CompletedTask; 120 | } 121 | }; 122 | options.TokenValidationParameters = new TokenValidationParameters 123 | { 124 | NameClaimType = "name", 125 | RoleClaimType = "groups", 126 | ValidateIssuer = true 127 | }; 128 | }); 129 | 130 | services.AddAuthorization(); 131 | services.AddHttpContextAccessor(); 132 | } 133 | 134 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 135 | { 136 | if (env.IsDevelopment()) 137 | { 138 | app.UseDeveloperExceptionPage(); 139 | } 140 | 141 | app.UseHttpsRedirection(); 142 | 143 | // added 144 | app.UseStaticFiles(); 145 | 146 | app.UseRouting(); 147 | app.UseAuthentication(); // added 148 | app.UseAuthorization(); 149 | 150 | app.UseEndpoints(endpoints => 151 | { 152 | endpoints.MapControllers(); 153 | 154 | // added 155 | endpoints.MapBlazorHub(); 156 | endpoints.MapFallbackToPage("/_Host"); 157 | }); 158 | } 159 | 160 | internal static Task UnAuthorizedResponse(RedirectContext context) 161 | { 162 | context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; 163 | return Task.CompletedTask; 164 | } 165 | 166 | internal static Task ForbiddenResponse(RedirectContext context) 167 | { 168 | context.Response.StatusCode = (int)HttpStatusCode.Forbidden; 169 | return Task.CompletedTask; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/WebApp (blazor-serverside).csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | web 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Common 2 | @using System.Net.Http 3 | @using System.Net.Http.Headers 4 | @using System.Security.Claims 5 | @using System.Security.Principal 6 | @using Microsoft.AspNetCore.Authentication 7 | @using Microsoft.AspNetCore.Authentication.OpenIdConnect 8 | @using Microsoft.AspNetCore.Authorization 9 | @using Microsoft.AspNetCore.Components 10 | @using Microsoft.AspNetCore.Components.Authorization 11 | @using Microsoft.AspNetCore.Components.Forms 12 | @using Microsoft.AspNetCore.Components.Routing 13 | @using Microsoft.AspNetCore.Components.Web 14 | @using Microsoft.AspNetCore.Http 15 | @using Microsoft.Extensions.Configuration 16 | @using Microsoft.JSInterop 17 | @using IdentityModel.Client 18 | @using Newtonsoft.Json 19 | @using Newtonsoft.Json.Linq 20 | @using web -------------------------------------------------------------------------------- /WebApp (blazor-serverside)/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Oidc": { 11 | "Authority": "https://global-keycloak.azurewebsites.net/auth/realms/master", 12 | "ClientId": "aspnetcore-keycloak", 13 | "ClientSecret": "1beb5df9-01dd-46c3-84a8-b65eca50ad57", 14 | "Resource": "aspnetcore-keycloak", 15 | "Scope": [ "openid", "profile", "email", "claims" ], 16 | "ResponseType": "code", // AAD= + id_token 17 | "GetClaimsFromUserInfoEndpoint": false, 18 | "SaveTokens": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /WebApp (html-js)/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 WebApp__html_js_ 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 | -------------------------------------------------------------------------------- /WebApp (html-js)/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true 5 | }, 6 | "profiles": { 7 | "WebApp__html_js_": { 8 | "commandName": "Project", 9 | "launchBrowser": true, 10 | "launchUrl": "index.html", 11 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 12 | "environmentVariables": { 13 | "ASPNETCORE_ENVIRONMENT": "Development" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebApp (html-js)/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | 11 | namespace WebApp__html_js_ 12 | { 13 | public class Startup 14 | { 15 | // This method gets called by the runtime. Use this method to add services to the container. 16 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 17 | public void ConfigureServices(IServiceCollection services) 18 | { 19 | } 20 | 21 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 22 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 23 | { 24 | if (env.IsDevelopment()) 25 | { 26 | app.UseDeveloperExceptionPage(); 27 | } 28 | 29 | app.UseStaticFiles(); 30 | app.UseRouting(); 31 | 32 | app.UseEndpoints(endpoints => 33 | { 34 | //endpoints.MapGet("/", async context => 35 | //{ 36 | // await context.Response.WriteAsync("Hello World!"); 37 | //}); 38 | }); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /WebApp (html-js)/WebApp (html-js).csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | WebApp__html_js_ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /WebApp (html-js)/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /WebApp (html-js)/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /WebApp (html-js)/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | WebApp (html-js) 6 | 43 | 44 | 45 | signin 46 | 47 | -------------------------------------------------------------------------------- /WebApp (html-js)/wwwroot/signin-oidc-callback.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 44 | 60 | 61 | 62 | 63 |
64 |

Client Side > Implicit Flow - Signin Callback

65 | home 66 |
67 | id_token claims and userinfo not found. 68 |
69 |
70 | 71 | -------------------------------------------------------------------------------- /WebApp (html-js)/wwwroot/signin-oidc.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 46 | 64 | 65 | 66 | 67 | 68 |
69 |

Client Side > Implicit Flow - Signin Page

70 | home 71 |
72 | clientInfo 73 |
74 | clientInfo not found 75 |
76 | 79 | loginRequest 80 |
81 | loginRequest not generated yet 82 |
83 | 86 |
87 |
88 | 93 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /aspnetcore-keycloak.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29512.175 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{0A0A87E1-D428-4A9F-9FF7-CDC59C2658E3}" 7 | ProjectSection(SolutionItems) = preProject 8 | README.md = README.md 9 | REST.http = REST.http 10 | EndProjectSection 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "WebApi\WebApi.csproj", "{50A74F5F-148E-4347-88DB-5D7D385D0D72}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp (blazor-serverside)", "WebApp (blazor-serverside)\WebApp (blazor-serverside).csproj", "{AEC35650-8F70-4601-9254-579E3EF1C8C5}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp (html-js)", "WebApp (html-js)\WebApp (html-js).csproj", "{06CDB21F-B3EE-4172-8DDD-7FE35538DB13}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp (angular)", "WebApp (angular)\WebApp (angular).csproj", "{A63AE592-7C8E-452F-BEF6-19CC7CBC9443}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{BF4D504C-4227-43BF-9602-F435790969F9}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiBearer", "WebApiBearer\WebApiBearer.csproj", "{53A8BE0B-9FB2-4C03-8218-5034D81266E3}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {50A74F5F-148E-4347-88DB-5D7D385D0D72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {50A74F5F-148E-4347-88DB-5D7D385D0D72}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {50A74F5F-148E-4347-88DB-5D7D385D0D72}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {50A74F5F-148E-4347-88DB-5D7D385D0D72}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {AEC35650-8F70-4601-9254-579E3EF1C8C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {AEC35650-8F70-4601-9254-579E3EF1C8C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {AEC35650-8F70-4601-9254-579E3EF1C8C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {AEC35650-8F70-4601-9254-579E3EF1C8C5}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {06CDB21F-B3EE-4172-8DDD-7FE35538DB13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {06CDB21F-B3EE-4172-8DDD-7FE35538DB13}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {06CDB21F-B3EE-4172-8DDD-7FE35538DB13}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {06CDB21F-B3EE-4172-8DDD-7FE35538DB13}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {A63AE592-7C8E-452F-BEF6-19CC7CBC9443}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {A63AE592-7C8E-452F-BEF6-19CC7CBC9443}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {A63AE592-7C8E-452F-BEF6-19CC7CBC9443}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {A63AE592-7C8E-452F-BEF6-19CC7CBC9443}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {BF4D504C-4227-43BF-9602-F435790969F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {BF4D504C-4227-43BF-9602-F435790969F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {BF4D504C-4227-43BF-9602-F435790969F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {BF4D504C-4227-43BF-9602-F435790969F9}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {53A8BE0B-9FB2-4C03-8218-5034D81266E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {53A8BE0B-9FB2-4C03-8218-5034D81266E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {53A8BE0B-9FB2-4C03-8218-5034D81266E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {53A8BE0B-9FB2-4C03-8218-5034D81266E3}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {33ACBB98-5154-48A0-B953-1F8FC619237C} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /keycloak_client_setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vip32/aspnetcore-keycloak/51c5433edefc54062e04dfd73e02ac7437baff8a/keycloak_client_setup.png --------------------------------------------------------------------------------