├── .dockerignore ├── .gitignore ├── Common ├── Common.csproj ├── DefaultHeaders.cs ├── HealthChecks │ └── HealthCheckBuilderExtensions.cs └── Models │ └── WeatherForecast.cs ├── Gateway ├── Certificates │ ├── aspnetapp-decrypted.key │ ├── aspnetapp.crt │ ├── aspnetapp.key │ └── aspnetapp.pfx ├── Controllers │ └── LoginController.cs ├── DataProtection │ └── key-60d7e46a-ec9d-404a-bb34-28f046d7a131.xml ├── Dockerfile ├── Gateway.csproj ├── Infrastructure │ ├── Authentication │ │ ├── ClaimHeaderAppender.cs │ │ └── CustomAuthenticationSchemeProvider.cs │ ├── Authorization │ │ └── Requirements │ │ │ ├── AdminRequirement.cs │ │ │ └── AdminRequirementHandler.cs │ ├── Interfaces │ │ └── Services │ │ │ └── IRefreshTokenService.cs │ └── Services │ │ ├── Abstract │ │ └── RefreshTokenService.cs │ │ ├── IdentityServerRefreshTokenService.cs │ │ └── OktaRefreshTokenService.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Settings │ └── IdentityServerSettings.cs ├── appsettings.Development.json ├── appsettings.Production.json └── appsettings.json ├── README.md ├── Weather.Api ├── Authentication │ ├── GatewayAuthenticationDefaults.cs │ ├── GatewayAuthenticationExtensions.cs │ ├── GatewayAuthenticationHandler.cs │ └── GatewayAuthenticationOptions.cs ├── Authorization │ ├── AdminRequirement.cs │ ├── AdminRequirementHandler.cs │ └── AuthorizationPolicies.cs ├── Controllers │ ├── AdminController.cs │ ├── DiagnosticsController.cs │ └── WeatherForecastController.cs ├── Dockerfile ├── Program.cs ├── Properties │ └── launchSettings.json ├── Weather.Api.csproj ├── appsettings.Development.json └── appsettings.json ├── Weather.Web ├── Client │ ├── App.razor │ ├── Pages │ │ ├── AccessDenied.razor │ │ ├── FetchData.razor │ │ └── Index.razor │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Shared │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ ├── NavMenu.razor.css │ │ └── SurveyPrompt.razor │ ├── Weather.Web.Client.csproj │ ├── _Imports.razor │ └── wwwroot │ │ ├── css │ │ ├── app.css │ │ ├── bootstrap │ │ │ ├── bootstrap.min.css │ │ │ └── bootstrap.min.css.map │ │ └── open-iconic │ │ │ ├── FONT-LICENSE │ │ │ ├── ICON-LICENSE │ │ │ ├── README.md │ │ │ └── font │ │ │ ├── css │ │ │ └── open-iconic-bootstrap.min.css │ │ │ └── fonts │ │ │ ├── open-iconic.eot │ │ │ ├── open-iconic.otf │ │ │ ├── open-iconic.svg │ │ │ ├── open-iconic.ttf │ │ │ └── open-iconic.woff │ │ ├── favicon.ico │ │ ├── icon-192.png │ │ └── index.html └── Server │ ├── Dockerfile │ ├── Pages │ ├── Error.cshtml │ └── Error.cshtml.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Weather.Web.Server.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── YarpSecurity.sln ├── YarpSecurity.sln.DotSettings ├── aspnetapp.crt ├── docker-compose.dcproj ├── docker-compose.yml └── k8s ├── api └── api.yml ├── ingress ├── README.md └── ingress.yml ├── k8s.projitems ├── k8s.shproj └── web └── web.yml /.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 | **/tests 25 | LICENSE 26 | README.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /Common/Common.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Common/DefaultHeaders.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace Common 3 | { 4 | public static class DefaultHeaders 5 | { 6 | public const string Name = "x-forwarded-name"; 7 | public const string Email = "x-forwarded-email"; 8 | public const string GivenName = "x-forwarded-givenname"; 9 | public const string Surname = "x-forwarded-surname"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Common/HealthChecks/HealthCheckBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.Extensions.Diagnostics.HealthChecks; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection 5 | { 6 | public static class HealthCheckBuilderExtensions 7 | { 8 | public static IHealthChecksBuilder AddLiveness(this IHealthChecksBuilder builder, params string[] additionalTags) 9 | { 10 | var tags = new List() { "liveness" }; 11 | tags.AddRange(additionalTags); 12 | builder.AddCheck("self", () => HealthCheckResult.Healthy("Application is running"), tags); 13 | return builder; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Common/Models/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace Common.Models 2 | { 3 | public class WeatherForecast 4 | { 5 | public DateTime Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Gateway/Certificates/aspnetapp-decrypted.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDM1U2J6wgi9BNS 3 | qDmQwIq3xNLmcezKa+UKmhAzUI3S3NQVp6XUEIUVv2DyAa0FrZoNLzAEP/0LtN38 4 | cJixS3do5pxhRSlNtAVdwtMLWiTnVaNv1opH6QLTysq9jariqV/qjtWxHIE3cxPX 5 | RmM9D4iWs5Z6lYq6pk8NWFOUCIHKoLzR2PdZJSuRq+C+vbZpOXP3bNsZLjalSZkH 6 | mFAZj0cnQOJoicipTtFf1SJWfYpQ9GduQhpV+tR42+aoI3nFQNS8c7aLRpXm0Vmh 7 | E+wD1O0lHgAukh+1ftiHY4sKyM4K0P2S3sGgWji6mEbVK6gA+KpEo16Toee1Utur 8 | 0KQMTf01AgMBAAECggEADarp+prxNwvcQjaxaKRAXVTH0dKmPzeF1/hBvnrQi9Jq 9 | YGuIWCcmCjZz7JIKDofv8JNGe/IdG2vdKBwHREVJ2XkR1De+Ms4hUYlPdcF1Fu6S 10 | 8kJ5haTZqHIupwlN7sPof4k6XxAM2ztVhpbOxmND9PJUxyc1U5EHU1eidA1BQ9jp 11 | STBpGCBtnC58GEJLrvGQR7vYTayv4LzjfCHkZdv+WVwGEczdhmxwvN4JoByri8mH 12 | uRar/lq4ZPkToPb4tj/Zb2BoD3zzm7FrWrv5ioaIPNyjrm26XVQj2g5y8s0qW8BJ 13 | CvpjanKqZeJS9799mVsSBlEP4G2hsBS4EHOOAVsRvQKBgQDvmr5ceHfNSL11lJ4m 14 | B6rCsQhJGRTMkwCPaoUiFLsY7lfEEApbk0DuKwnLeF6hrwzOVjHfF7Z2bUl92uxo 15 | QjjYB0ir0Z2sWgZV3a3vtYMqWohyRReOnbKEiljOWa46xeFGlZu/EZtttLtFo9Yl 16 | saVV7to8Wf4GmjOkF6Fg4YzOpwKBgQDa2XS92o7xPupN97JUa8mhVZsv2u0f/fw3 17 | +hMXFddjW16V1plXO8G3V0nCVxuXhry3UtDEozVlQ4JzdCOX2cGUqsHRa3LBllVn 18 | KC/RqKTYMb2fh2YH7Y9+TVe34HT0CuJwI6OyXa7hAI1lwxMLbAesEMK9zG5sr0P4 19 | 7hFDOxVMwwKBgQCZXrYHPt2Au9T6VJ0+vp2hbexdzf4wTNCI/KgnfpmchcQMLjBT 20 | GdjwGwB0C6KXYBOUaDwYD3euKppmQyIcQndcYJ01zkFdN2riv7k8vTUugTDh5DQG 21 | CA9jI6Ngwqrx2AQxSEafcUfKW2ucUhkbm7C3ZuKuyvP9iesQWEfmWbnl9QKBgEcZ 22 | 1x/HXiFqu1zRWJKs4jkUQvLtQZTfQ448JO+fHPcevEAaFH+O3oTeSwxkmIZGNBGr 23 | SkBYdgSMRlmUJewzxvyx7X5IVmQmn2CAYejPzZ6fqmel6bQxrijun80Rk22mC6M7 24 | MPfGEAbx0v54vjs7wdJZL6vSOx38Yy0Ay4L4MxdZAoGBAK4ZWNb9VVEDrHfZyFSu 25 | o74p5YrMEtwRoK3E3wFPK0EkzKBh9Y6v91+/NL9GIN9qGsdYmN8slLUqKP4RxhQ2 26 | 0r0rlOLunB21gAk49c7d50oT3ci1zY6ODnU/Mme0XBF2RWcQ3TLkZD4fX0Ssg/Pi 27 | qBxXAxsRZD5/1lWlVhGd+pU+ 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /Gateway/Certificates/aspnetapp.crt: -------------------------------------------------------------------------------- 1 | Bag Attributes 2 | localKeyID: 01 00 00 00 3 | friendlyName: ASP.NET Core HTTPS development certificate 4 | subject=CN = localhost 5 | issuer=CN = localhost 6 | -----BEGIN CERTIFICATE----- 7 | MIIDDTCCAfWgAwIBAgIJAJeOjRiL5W7sMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 8 | BAMTCWxvY2FsaG9zdDAeFw0yMjA5MDYxODM2MTJaFw0yMzA5MDYxODM2MTJaMBQx 9 | EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 10 | ggEBAMzVTYnrCCL0E1KoOZDAirfE0uZx7Mpr5QqaEDNQjdLc1BWnpdQQhRW/YPIB 11 | rQWtmg0vMAQ//Qu03fxwmLFLd2jmnGFFKU20BV3C0wtaJOdVo2/WikfpAtPKyr2N 12 | quKpX+qO1bEcgTdzE9dGYz0PiJazlnqVirqmTw1YU5QIgcqgvNHY91klK5Gr4L69 13 | tmk5c/ds2xkuNqVJmQeYUBmPRydA4miJyKlO0V/VIlZ9ilD0Z25CGlX61Hjb5qgj 14 | ecVA1LxztotGlebRWaET7APU7SUeAC6SH7V+2IdjiwrIzgrQ/ZLewaBaOLqYRtUr 15 | qAD4qkSjXpOh57VS26vQpAxN/TUCAwEAAaNiMGAwDAYDVR0TAQH/BAIwADAOBgNV 16 | HQ8BAf8EBAMCBaAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwFwYDVR0RAQH/BA0w 17 | C4IJbG9jYWxob3N0MA8GCisGAQQBgjdUAQEEAQIwDQYJKoZIhvcNAQELBQADggEB 18 | AEsNwQrigdo+V7EZagboeOmsgJU1dzgDmlDuQX/rjZn7ryXAEanJTweohRvjeExY 19 | 5qUlqRKvnjJdtvs44CBtKUjo6pfVhd4ON0rxR3jklusPbH/oeCP8JUjYO25JpzI8 20 | 6sD6ftFR2wa7hqIEThUW+Ixkz3RKzYRYTtz/SL+xfVGmaeKo2Z4KdGmbu4o4zHJU 21 | WL/lrxtswDOX6V3W3yKXbvdAhTzOMl/dbNdPJn5i4Lr3+oQeRCXZislFj8Ocy1Yt 22 | 99q/ussdmyvJYQbYRLMCsyZUQey3Xe2u2dEYBkARnLQIAFrhjbF5FtU/7QEUJ61X 23 | jofKYCS4YAfb+lFSbWHReL4= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /Gateway/Certificates/aspnetapp.key: -------------------------------------------------------------------------------- 1 | Bag Attributes 2 | localKeyID: 01 00 00 00 3 | friendlyName: {49E87AFA-F6E3-45F4-91F2-6523AB6EA520} 4 | Microsoft CSP Name: Microsoft Software Key Storage Provider 5 | Key Attributes: 6 | -----BEGIN ENCRYPTED PRIVATE KEY----- 7 | MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIE8Pcoczn0WACAggA 8 | MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBA35JiFGHFHJTpybi3T/LVXBIIE 9 | 0MVYv5wjNWn9IByzukeRCkmSDGxJk8smOQPwU0S8+rcKXOZtGLe0DS/4c+7cBZ/o 10 | D7f3s0O5vIxLBRFwd/sRijtFDdAEwEIhRXxhn/l9s0j3Hb4+4y9v99GppMrbJZ8k 11 | 65D23sKGVgNdhAHYHYF4UYDHIx1n/tFbItHB9XrUF5SirjtJYUcZfVxht574aWVG 12 | Oc6P7UoB7wMZt0gwa7cjVc1z4T8T5TWOXN4ps0kCBX4wriZXo92dX57Vjn2jLfDf 13 | P3xVENnU3V80Ci3WpzewFaVAo1sNcAHG1idnvHIrEAp9VpN2zkpVJJR8Xaj8/3+7 14 | 0k5bkV7ikgioLJmJ1mxr7rUUrZllD0pDrXVOIeowCVxMcBZE/5v3hY1xldVdYt3W 15 | qHDLGcHdP7KKTLUvprKn4szChah12lW38jLdLLdGetNybCvbPJwpQtKHadtr/Hw1 16 | awby+Dr8xdrmJd9f9nDcHMc48+iwUq7wIKOtPCYiJPO5bQj1EuMTKAcodBHZJzvs 17 | D8jhUehHWj5tIk6HdqdVYbjtP66L8PMSVCj8PqRn4AtGYhRGV4H+GX6ewUcumrbe 18 | gqSkhdtqslAIelj1qMWeHHajxY6KGgPJO3kB+uckl0abFnj6JmOzIQhM7A5C2MIy 19 | T1ehw4skoJsT7b804tQSaNC+QHUby+gpiNu7E/J8SwGeitXbI0vmUJJjAU0D2uQy 20 | Yjg8fTOBZMdyLK+m/noWMqVwbOlXHvE5J29H5XXR0LNb0JAJccxgO5PrVeuQ0Hve 21 | RBkXZO3JDoRWtxE5BwGHU9Cq2BcB+Q3BBXaO/3KyWdhMguLRI5ZfBOs02sHLDSaK 22 | CET/0gnhkeO4OZxNGxWe1NBUsCkIzo/RuXkPWC44UyAlS1hzBb/1oxvwktIrjNSa 23 | 45457M5BfxetKfwtSqyyce2IUEb1SrduV36PiaK0zPWTD9vNOTfxvR+/2LWDR21e 24 | 7CrM9ZQ55HM3mGsKiUfL8QFNXufoEaMSghEkYrOyDKHRBdNzEBBOhp0V4rW8DMw0 25 | Dgbhdin5uEMQQFEeIfCwchtCgCl9siqHkzuanaMb5wJShVy2b5quPOVhLPuyC6O2 26 | TD11ZvYC6YcaGHsVsr1kfgdcM4d+7YnhXSBDEIS6eEFhkS8aBtzbX49Qia+RRTz0 27 | Mv3uFrh0peTPbrAWLUq9PSf19IppXeiXVWIK9FH9p5iFuvFPPfvfqTDM6UqKbjOt 28 | JEIA3eomOfUMmL8ycpUva4hCR3k+kAT3N6lLVxL07gkGJDt7BWwSRGoHZaAYtF5s 29 | i3TJTT95Drp3YBJ9d2NmiEIkjsErVMM7sm+8oq+GJkZJiHcw5I4ckRwgM0wfv/zQ 30 | pzANlL0b85GYxAVtnAVFj15ERKCqu1UqSNZay3HfrtHj3CqwccmD/+jAXnkhGz5+ 31 | QiJES7mkolgUQ4A2rrI9moZp8iYLkJSuQsjPZJ4AeXgAFloGGgUS6ZCDOt8RAst+ 32 | 5aYpQeI8VqHEf/m4L1Cr5ZUmHc2R5AFor20eMWwG39bkWH6mBQPVVr9ESflgOa0u 33 | 0EQaGhrVIh6o2LHSYdM1kaYB2wbqqpvsJ+SuE1sORHAi8HyjOVFEmaEPKt4Tgn1H 34 | st5w7GWwyijoAP4HUjbROd3mR/rDjqkWaPTXk/pJ26V2 35 | -----END ENCRYPTED PRIVATE KEY----- 36 | -------------------------------------------------------------------------------- /Gateway/Certificates/aspnetapp.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-bentley/AuthenticationProxy/2840a4abdcc07e91a328dec493aafa758b600d42/Gateway/Certificates/aspnetapp.pfx -------------------------------------------------------------------------------- /Gateway/Controllers/LoginController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Gateway.Controllers; 8 | 9 | [Controller] 10 | public class LoginController : Controller 11 | { 12 | [HttpGet("/login")] 13 | [Authorize] 14 | public IActionResult Login(string returnUrl) 15 | { 16 | if (returnUrl != null) 17 | { 18 | return Redirect(returnUrl); 19 | } 20 | return Ok(User.Identity?.Name); 21 | } 22 | 23 | [HttpGet("/logout")] 24 | [Authorize] 25 | public IActionResult Logout(string returnUrl) 26 | { 27 | return new SignOutResult(new [] 28 | { 29 | OpenIdConnectDefaults.AuthenticationScheme, 30 | CookieAuthenticationDefaults.AuthenticationScheme 31 | }, 32 | new AuthenticationProperties() 33 | { 34 | RedirectUri = returnUrl 35 | }); 36 | } 37 | 38 | 39 | [HttpGet("/whoami")] 40 | public List WhoAmI() 41 | { 42 | return User.Claims.Select(x => new ClaimRecord(x.Type, x.Value)).ToList(); 43 | } 44 | 45 | [HttpGet("/session")] 46 | public async Task> Session() 47 | { 48 | return (await HttpContext.AuthenticateAsync()).Properties.Items; 49 | } 50 | 51 | [HttpGet("/token")] 52 | public async Task Token() 53 | { 54 | var accessToken = await HttpContext.GetTokenAsync("access_token"); 55 | return Ok(accessToken); 56 | } 57 | 58 | public record ClaimRecord(string Type, object Value); 59 | } -------------------------------------------------------------------------------- /Gateway/DataProtection/key-60d7e46a-ec9d-404a-bb34-28f046d7a131.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 2022-09-07T17:19:34.5528469Z 4 | 2022-09-07T17:19:34.5168203Z 5 | 2050-01-23T17:19:34.5168203Z 6 | 7 | 8 | 9 | 10 | 11 | 12 | q22gIxbqdtRN2eI+ulz17mwcd7sR+IZI7wnVNNo0tYGu3VmlA6e7JniioLNc7YWdWjp/Y34diliVJ6o/waNJ/A== 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base 2 | WORKDIR /app 3 | EXPOSE 8081 4 | 5 | ENV ASPNETCORE_URLS=http://+:8081 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build 8 | 9 | COPY [".", "/src/"] 10 | 11 | WORKDIR src/Gateway 12 | RUN dotnet restore "Gateway.csproj" 13 | 14 | RUN dotnet publish "Gateway.csproj" --no-restore -c Release -o /app/publish 15 | 16 | FROM base AS final 17 | WORKDIR /app 18 | COPY --from=build /app/publish . 19 | USER 1000 20 | ENTRYPOINT ["dotnet", "Gateway.dll"] -------------------------------------------------------------------------------- /Gateway/Gateway.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Never 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Gateway/Infrastructure/Authentication/ClaimHeaderAppender.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | using System.Security.Claims; 3 | using Common; 4 | using Microsoft.AspNetCore.Authentication; 5 | using Microsoft.Net.Http.Headers; 6 | using Yarp.ReverseProxy.Transforms; 7 | 8 | namespace Gateway.Authentication 9 | { 10 | public class ClaimHeaderAppender : RequestTransform 11 | { 12 | public override async ValueTask ApplyAsync(RequestTransformContext context) 13 | { 14 | context.ProxyRequest.Headers.Remove(HeaderNames.Cookie); 15 | context.ProxyRequest.Headers.Remove(HeaderNames.Authorization); 16 | var user = context.HttpContext.User; 17 | if (user.Identity?.IsAuthenticated ?? false) 18 | { 19 | AddClaim(context, DefaultHeaders.Name, ClaimTypes.Name, ClaimTypes.NameIdentifier); 20 | AddClaim(context, DefaultHeaders.Email, ClaimTypes.Email); 21 | AddClaim(context, DefaultHeaders.GivenName, ClaimTypes.GivenName, "given_name"); 22 | AddClaim(context, DefaultHeaders.Surname, ClaimTypes.Surname, "family_name"); 23 | var accessToken = await context.HttpContext.GetTokenAsync("access_token"); 24 | if (!string.IsNullOrEmpty(accessToken)) 25 | { 26 | context.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 27 | } 28 | } 29 | } 30 | 31 | private void AddClaim(RequestTransformContext context, string forwardHeaderName, params string[] claimTypes) 32 | { 33 | context.ProxyRequest.Headers.Remove(forwardHeaderName); 34 | 35 | foreach (var claimType in claimTypes) 36 | { 37 | var claim = context.HttpContext.User.FindFirst(e => e.Type == claimType); 38 | if (claim is not null) 39 | { 40 | context.ProxyRequest.Headers.Add(forwardHeaderName, claim.Value); 41 | break; 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Gateway/Infrastructure/Authentication/CustomAuthenticationSchemeProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 5 | using Microsoft.Extensions.Options; 6 | using Microsoft.Net.Http.Headers; 7 | 8 | namespace Gateway.Authentication 9 | { 10 | public class CustomAuthenticationSchemeProvider : AuthenticationSchemeProvider 11 | { 12 | private readonly IHttpContextAccessor _httpContextAccessor; 13 | 14 | public CustomAuthenticationSchemeProvider( 15 | IHttpContextAccessor httpContextAccessor, 16 | IOptions options) 17 | : base(options) 18 | { 19 | _httpContextAccessor = httpContextAccessor; 20 | } 21 | 22 | private bool IsBearer 23 | { 24 | get 25 | { 26 | string authorization = _httpContextAccessor.HttpContext?.Request.Headers[HeaderNames.Authorization]; 27 | return !string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "); 28 | } 29 | } 30 | 31 | public override async Task GetDefaultAuthenticateSchemeAsync() 32 | { 33 | var schemeName = IsBearer ? JwtBearerDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme; 34 | return await GetSchemeAsync(schemeName); 35 | } 36 | 37 | public override async Task GetDefaultChallengeSchemeAsync() 38 | { 39 | var schemeName = IsBearer ? JwtBearerDefaults.AuthenticationScheme : OpenIdConnectDefaults.AuthenticationScheme; 40 | return await GetSchemeAsync(schemeName); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Gateway/Infrastructure/Authorization/Requirements/AdminRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Gateway.Infrastructure.Authorization.Requirements 4 | { 5 | public class AdminRequirement : IAuthorizationRequirement 6 | { 7 | public readonly string[] Administrators; 8 | 9 | public AdminRequirement(string[] administrators) 10 | { 11 | Administrators = administrators; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Gateway/Infrastructure/Authorization/Requirements/AdminRequirementHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using System.Security.Claims; 3 | 4 | namespace Gateway.Infrastructure.Authorization.Requirements 5 | { 6 | public class AdminRequirementHandler : AuthorizationHandler 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public AdminRequirementHandler(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement) 16 | { 17 | var emailClaim = context.User.FindFirst(e => e.Type == ClaimTypes.Email); 18 | if (context.User.Identity.IsAuthenticated && emailClaim != null) 19 | { 20 | var email = emailClaim.Value; 21 | if(requirement.Administrators.Any(e => e.Equals(email, StringComparison.OrdinalIgnoreCase))) 22 | { 23 | context.Succeed(requirement); 24 | } 25 | else 26 | { 27 | _logger.LogWarning("User is not an Administrator: {email}", email); 28 | context.Fail(); 29 | } 30 | } 31 | else 32 | { 33 | context.Fail(); 34 | } 35 | return Task.CompletedTask; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Gateway/Infrastructure/Interfaces/Services/IRefreshTokenService.cs: -------------------------------------------------------------------------------- 1 | using IdentityModel.Client; 2 | 3 | namespace Gateway.Interfaces.Services 4 | { 5 | public interface IRefreshTokenService 6 | { 7 | Task RefreshAsync(string refreshToken); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Gateway/Infrastructure/Services/Abstract/RefreshTokenService.cs: -------------------------------------------------------------------------------- 1 | using Gateway.Interfaces.Services; 2 | using Gateway.Settings; 3 | using IdentityModel.Client; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Gateway.Services.Abstract 7 | { 8 | public abstract class RefreshTokenService : IRefreshTokenService 9 | { 10 | protected readonly IOptions Options; 11 | protected readonly HttpClient Client; 12 | 13 | public RefreshTokenService(IOptions options, 14 | HttpClient client) 15 | { 16 | Options = options; 17 | Client = client; 18 | } 19 | 20 | public abstract Task RefreshAsync(string refreshToken); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Gateway/Infrastructure/Services/IdentityServerRefreshTokenService.cs: -------------------------------------------------------------------------------- 1 | using Gateway.Services.Abstract; 2 | using Gateway.Settings; 3 | using IdentityModel.Client; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Gateway.Services 7 | { 8 | public class IdentityServerRefreshTokenService : RefreshTokenService 9 | { 10 | public IdentityServerRefreshTokenService(IOptions options, HttpClient client) : base(options, client) 11 | { 12 | 13 | } 14 | 15 | public override async Task RefreshAsync(string refreshToken) 16 | { 17 | return await Client.RequestRefreshTokenAsync(new RefreshTokenRequest 18 | { 19 | Address = $"{Options.Value.Authority}/token", 20 | ClientId = Options.Value.ClientId, 21 | ClientSecret = Options.Value.ClientSecret, 22 | RefreshToken = refreshToken 23 | }).ConfigureAwait(false); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Gateway/Infrastructure/Services/OktaRefreshTokenService.cs: -------------------------------------------------------------------------------- 1 | using Gateway.Services.Abstract; 2 | using Gateway.Settings; 3 | using IdentityModel.Client; 4 | using Microsoft.Extensions.Options; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | 8 | namespace Gateway.Services 9 | { 10 | public class OktaRefreshTokenService : RefreshTokenService 11 | { 12 | private readonly AuthenticationHeaderValue _authenticationHeader; 13 | 14 | public OktaRefreshTokenService(IOptions options, HttpClient client) : base(options, client) 15 | { 16 | var authenticationString = $"{Options.Value.ClientId}:{Options.Value.ClientSecret}"; 17 | var base64EncodedAuthenticationString = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString)); 18 | _authenticationHeader = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); 19 | } 20 | 21 | public override async Task RefreshAsync(string refreshToken) 22 | { 23 | var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{Options.Value.Authority}/v1/token") 24 | { 25 | Content = new FormUrlEncodedContent(new Dictionary 26 | { 27 | {"grant_type", "refresh_token"}, 28 | {"refresh_token", refreshToken}, 29 | {"scope", "offline_access openid"}, 30 | {"redirect_uri", Options.Value.RedirectUri} 31 | }) 32 | }; 33 | requestMessage.Headers.Authorization = _authenticationHeader; 34 | 35 | var response = await Client.SendAsync(requestMessage).ConfigureAwait(false); 36 | return await TokenResponse.FromHttpResponseAsync(response).ConfigureAwait(false); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Gateway/Program.cs: -------------------------------------------------------------------------------- 1 | using Gateway; 2 | using Gateway.Interfaces.Services; 3 | using Gateway.Services; 4 | using Gateway.Settings; 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.AspNetCore.Authentication.Cookies; 7 | using Microsoft.AspNetCore.Authentication.JwtBearer; 8 | using Microsoft.AspNetCore.DataProtection; 9 | using Microsoft.AspNetCore.HttpOverrides; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.IdentityModel.Tokens; 12 | using System.Net; 13 | using Gateway.Authentication; 14 | using Gateway.Infrastructure.Authorization.Requirements; 15 | using Microsoft.AspNetCore.Authorization; 16 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 17 | using HealthChecks.UI.Client; 18 | 19 | var builder = WebApplication.CreateBuilder(args); 20 | 21 | var identityServerSettings = new IdentityServerSettings(); 22 | builder.Configuration.GetSection("IdentityServer").Bind(identityServerSettings); 23 | builder.Services.AddSingleton(Options.Create(identityServerSettings)); 24 | 25 | builder.Services.AddControllers(); 26 | builder.Services.AddHttpClient("RefreshToken"); 27 | builder.Services.AddSingleton(); 28 | builder.Services.AddSingleton(); 29 | 30 | builder.Services.AddDataProtection() 31 | .DisableAutomaticKeyGeneration() 32 | .PersistKeysToFileSystem(new DirectoryInfo(Path.Join(Directory.GetCurrentDirectory(), "DataProtection"))); 33 | 34 | if (identityServerSettings.IdentityProvider.Equals("Okta", StringComparison.OrdinalIgnoreCase)) 35 | { 36 | builder.Services.AddHttpClient(); 37 | } 38 | else 39 | { 40 | builder.Services.AddHttpClient(); 41 | } 42 | 43 | builder.Services.AddAuthentication() 44 | .AddOpenIdConnect(options => 45 | { 46 | options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 47 | options.Authority = identityServerSettings.Authority; 48 | options.ClientId = identityServerSettings.ClientId; 49 | options.ClientSecret = identityServerSettings.ClientSecret; 50 | options.ResponseType = "code"; 51 | options.Scope.Add("openid"); 52 | options.Scope.Add("profile"); 53 | options.Scope.Add("email"); 54 | options.Scope.Add("offline_access"); 55 | 56 | // This aligns the life of the cookie with the life of the token. 57 | // Note this is not the actual expiration of the cookie as seen by the browser. 58 | // It is an internal value stored in "expires_at". 59 | options.UseTokenLifetime = false; 60 | 61 | // used to store access_token and refresh_token in cookie 62 | // needed so that other instances can refresh 63 | options.SaveTokens = true; 64 | 65 | options.GetClaimsFromUserInfoEndpoint = true; 66 | }) 67 | .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => 68 | { 69 | options.LoginPath = "/login"; 70 | 71 | options.Events = new CookieAuthenticationEvents 72 | { 73 | // After the auth cookie has been validated, this event is called. 74 | // In it we see if the access token is close to expiring. If it is 75 | // then we use the refresh token to get a new access token and save them. 76 | // If the refresh token does not work for some reason then we redirect to 77 | // the login screen. 78 | OnValidatePrincipal = async cookieCtx => 79 | { 80 | var now = DateTimeOffset.UtcNow; 81 | var expiresAt = cookieCtx.Properties.GetTokenValue("expires_at"); 82 | var accessTokenExpiration = DateTimeOffset.Parse(expiresAt); 83 | var timeRemaining = accessTokenExpiration.Subtract(now); 84 | // TODO: Get this from configuration with a fall back value. 85 | var refreshThresholdSeconds = 60; 86 | var refreshThreshold = TimeSpan.FromSeconds(refreshThresholdSeconds); 87 | 88 | if (timeRemaining < refreshThreshold) 89 | { 90 | var refreshToken = cookieCtx.Properties.GetTokenValue("refresh_token"); 91 | var refreshTokenService = cookieCtx.HttpContext.RequestServices.GetRequiredService(); 92 | var response = await refreshTokenService.RefreshAsync(refreshToken); 93 | 94 | if (!response.IsError) 95 | { 96 | var expiresInSeconds = response.ExpiresIn; 97 | var updatedExpiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresInSeconds); 98 | cookieCtx.Properties.UpdateTokenValue("expires_at", updatedExpiresAt.ToString()); 99 | cookieCtx.Properties.UpdateTokenValue("access_token", response.AccessToken); 100 | cookieCtx.Properties.UpdateTokenValue("refresh_token", response.RefreshToken); 101 | 102 | // Indicate to the cookie middleware that the cookie should be remade (since we have updated it) 103 | cookieCtx.ShouldRenew = true; 104 | } 105 | else 106 | { 107 | cookieCtx.RejectPrincipal(); 108 | } 109 | } 110 | } 111 | }; 112 | }) 113 | .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => 114 | { 115 | options.Authority = identityServerSettings.Authority; 116 | options.Audience = identityServerSettings.Audience; 117 | options.TokenValidationParameters = new TokenValidationParameters 118 | { 119 | ValidateIssuer = true, 120 | ValidateAudience = true, 121 | ValidateIssuerSigningKey = true 122 | }; 123 | }); 124 | 125 | builder.Services.AddTransient(); 126 | builder.Services.AddAuthorization(options => 127 | { 128 | options.FallbackPolicy = new AuthorizationPolicyBuilder() 129 | .RequireAuthenticatedUser() 130 | .Build(); 131 | 132 | options.AddPolicy("admin", p => 133 | { 134 | p.RequireAuthenticatedUser(); 135 | p.AddRequirements(new AdminRequirement(builder.Configuration.GetSection("Administrators").Get())); 136 | }); 137 | }); 138 | 139 | builder.Services.AddSingleton(); 140 | builder.Services.AddReverseProxy() 141 | .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) 142 | .AddTransforms(context => 143 | { 144 | context.RequestTransforms.Add(context.Services.GetRequiredService()); 145 | }); 146 | 147 | builder.Services.AddHealthChecks() 148 | .AddLiveness("gateway"); 149 | 150 | // this is needed to forward the host headers when reverse proxied through load balancers 151 | builder.Services.Configure(options => 152 | { 153 | options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; 154 | options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("::ffff:10.0.0.0"), 104)); 155 | options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("::ffff:192.168.0.0"), 112)); 156 | options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("::ffff:172.16.0.0"), 108)); 157 | }); 158 | 159 | var app = builder.Build(); 160 | 161 | // Required to forward headers from load balancers and reverse proxies 162 | app.UseForwardedHeaders(); 163 | 164 | app.UseCors(p => p 165 | .AllowAnyHeader() 166 | .WithOrigins("https://localhost:8080") 167 | .AllowAnyMethod() 168 | .AllowCredentials()); 169 | 170 | app.UseRouting(); 171 | app.UseAuthentication(); 172 | app.UseAuthorization(); 173 | app.MapControllers(); 174 | 175 | app.MapHealthChecks("/gateway/liveness", new HealthCheckOptions 176 | { 177 | Predicate = r => r.Name.Contains("self") 178 | }).AllowAnonymous(); 179 | app.MapHealthChecks("/gateway/healthz", new HealthCheckOptions() 180 | { 181 | Predicate = _ => true, 182 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 183 | }).AllowAnonymous(); 184 | 185 | app.MapReverseProxy(); 186 | app.Run(); -------------------------------------------------------------------------------- /Gateway/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "profiles": { 4 | "Gateway": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "https://localhost:9090;http://localhost:5141", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Gateway/Settings/IdentityServerSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Gateway.Settings 2 | { 3 | public class IdentityServerSettings 4 | { 5 | public string Authority { get; set; } 6 | public string Audience { get; set; } 7 | public string ClientId { get; set; } 8 | public string ClientSecret { get; set; } 9 | public string RedirectUri { get; set; } 10 | public string IdentityProvider { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Gateway/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft.AspNetCore": "Debug" 6 | } 7 | }, 8 | "ReverseProxy": { 9 | "Routes": { 10 | "weatherapi-admin": { 11 | "ClusterId": "weatherservice", 12 | "AuthorizationPolicy": "admin", 13 | "Match": { 14 | "Path": "/api/admin/{**catch-all}" 15 | } 16 | }, 17 | "weatherapi-health": { 18 | "ClusterId": "weatherservice", 19 | "AuthorizationPolicy": "anonymous", 20 | "Match": { 21 | "Path": "/api/healthz" 22 | } 23 | }, 24 | "weatherapi-liveness": { 25 | "ClusterId": "weatherservice", 26 | "AuthorizationPolicy": "anonymous", 27 | "Match": { 28 | "Path": "/api/liveness" 29 | } 30 | }, 31 | "weatherapi": { 32 | "ClusterId": "weatherservice", 33 | "Match": { 34 | "Path": "/api/{**catch-all}" 35 | } 36 | }, 37 | "weatherswagger": { 38 | "ClusterId": "weatherservice", 39 | "Match": { 40 | "Path": "/swagger/{**catch-all}" 41 | } 42 | }, 43 | "weatherswagger2": { 44 | "ClusterId": "weatherservice", 45 | "Match": { 46 | "Path": "/swagger" 47 | } 48 | }, 49 | "weatherapp": { 50 | "ClusterId": "weatherapp", 51 | "Match": { 52 | "Path": "/{**catch-all}" 53 | } 54 | } 55 | }, 56 | "Clusters": { 57 | "weatherservice": { 58 | "Destinations": { 59 | "weatherservice-1": { 60 | "Address": "http://localhost:5149/" 61 | } 62 | } 63 | }, 64 | "weatherapp": { 65 | "Destinations": { 66 | "weatherapp-1": { 67 | "Address": "http://localhost:5022/" 68 | } 69 | } 70 | } 71 | } 72 | }, 73 | "Administrators": [ 74 | "admin@dev-28752567-admin.okta.com" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /Gateway/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "ReverseProxy": { 3 | "Routes": { 4 | "downstream": { 5 | "ClusterId": "downstream", 6 | "AuthorizationPolicy": "default", 7 | "Match": { 8 | "Path": "/{**catch-all}" 9 | } 10 | } 11 | }, 12 | "Clusters": { 13 | "downstream": { 14 | "Destinations": { 15 | "downstream-1": { 16 | "Address": "http://localhost:8080/" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Gateway/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "Microsoft.AspNetCore": "Debug" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "IdentityServer": { 10 | "Authority": "https://dev-28752567.okta.com/oauth2/default", 11 | "Audience": "api://default", 12 | "ClientId": "0oa6f14ttjMnCgP595d7", 13 | "ClientSecret": "ZmKFG2isF9z6r268XSDV3RWI08dM7TJTEx6oDA1a", 14 | "RedirectUri": "https://localhost:9090/signin-oidc", 15 | "IdentityProvider": "Okta" 16 | }, 17 | "Administrators": [] 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Authentication Proxy 2 | 3 | This project uses YARP (Yet Another Reverse Proxy) to offload authentication at a gateway proxy so that downstream applications don't need to have any authentication code/logic. 4 | 5 | This solution can be used as an API gateway or as a sidecar proxy for Kubernetes pods. 6 | 7 | The user's identity claims are passed down to the downstream services as headers. 8 | 9 | ## Authentication 10 | 11 | The gateway uses **OpenId Connect Authorization Code Flow** to challenge/authenticate unauthenticated users. The *offline_access* scope is used to retrieve refresh tokens so that the gateway can refresh access_tokens when they are due to expire. 12 | 13 | The gateway can authenticate requests using both Cookies and JWT bearer tokens. The default challenge method is cookies. 14 | 15 | ### Cookies 16 | 17 | Cookies are used to authenticate requests through the gateway by default rather than JWTs. Cookies are used by default for the following reasons: 18 | - Client applications will not need any logic for handling the tokens 19 | - Cookies are stored as `Secure`, `Lax` and `HttpOnly` so that they cannot be retrieved in a XSS attack unlike JWTs which are normally stored in local storage. 20 | 21 | The *access_token* and *refresh_token* is stored in the session cookie so that any Gateway instance can extract and refresh the access_token. This allows the gateway to be load balanced or used as a sidecar container to multiple applications. 22 | 23 | The Data Protection key is stored inside the Gateway container so that the same key is used for creating cookies for all instances. In a production system the data protection keys should be rotated and stored securely using a system such as Azure Key Vault. 24 | 25 | ### JWT Bearer 26 | 27 | Any requests that container an `Authorization` header with a **Bearer** token will be authenticated using the JWT token rather than a cookie. This is useful for server-to-server requests. 28 | 29 | ## Forwarded Claims/Headers 30 | 31 | Headers were chosen as the mechanism to pass through the identity information so that no additional configuration is needed for the downstream applications. JWTs could be used for the claims however the downsteam apps would need to know about the issuing authority and how to validate the JWTs. 32 | 33 | The claims are serialized into an identity using the ***GatewayAuthenticationHandler***. 34 | 35 | The access_token is passed in via the **Authorization** header to downstream requests so that the downstream applications can use it to make additional requests for the user. The access_token is passed downstream regardless of whether Cookies or JWTs are used to authenticate the user. 36 | 37 | The following claims are passed through the gateway. If any of these headers are passed in the original request they will be removed by the gateway. 38 | 39 | Header Name | Description | Example | 40 | --- | --- |--- | 41 | x-forwarded-name | Username | Test User | 42 | x-forwarded-email | Email | user@dev-28752567-admin.okta.com | 43 | x-forwarded-givenname | First name | Test | 44 | x-forwarded-surname | Last name | User | 45 | Authorization | JWT Bearer token | Bearer {access_token} | 46 | 47 | ## Build & Run 48 | 49 | The solution can be run via Visual Studio or in Kubernetes. 50 | 51 | ### Run in Visual Studio 52 | 53 | To run in Visual Studio the following projects must be set as startup projects: 54 | 55 | - Gateway 56 | - Weather.Api 57 | - Weather.Web.Server 58 | 59 | This will run a single Gateway instance which can route to either the API or Web application using path based routing. 60 | 61 | ### Run in Kubernetes 62 | 63 | In Kubernetes the Gateway runs as a sidecar container on each of the API and Web pods. An Nginx ingress controller is used as the frontend reverse proxy to offload SSL and route traffic to the API and Web pods. 64 | 65 | #### Build Containers 66 | 67 | The Docker containers can be built using the following: 68 | ```bash 69 | docker-compose build 70 | ``` 71 | 72 | #### Trust Dev Certificate 73 | 74 | A self-signed certificate has been generated using dotnet dev-certs. This must be trusted to stop SSL errors appearing in the browser when accessing. Follow these steps if using Windows: 75 | - Double click on `aspnetapp.crt` 76 | - Click 'Install Certificate' 77 | - Select Store Location 'Current User' 78 | - Select 'Place all certificates in the following store' then 'Browse' 79 | - Select 'Trusted Root Certification Authorities' 80 | - Accept the security warning to install 81 | 82 | #### Deploy to Kubernetes 83 | 84 | The following can be used to install the Kubernetes components after the containers have been built: 85 | 86 | ```bash 87 | # install nginx ingress 88 | helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx 89 | helm repo update 90 | helm install nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginx 91 | 92 | # deploy ingress SSL certificate and ingress routes 93 | kubectl apply -f k8s\ingress\ingress.yml 94 | 95 | # deploy api pod 96 | kubectl apply -f k8s\api\api.yml 97 | 98 | # deploy web pod 99 | kubectl apply -f k8s\web\web.yml 100 | ``` 101 | 102 | ### Application Endpoints 103 | 104 | When debugging via Visual Studio the url will be '**https://localhost:9090**' and when accessing via Kubernetes the url will be '**https://localhost**'. 105 | 106 | The application can be accessed using the following urls: 107 | 108 | Description | Url | 109 | --- | --- | 110 | Blazor Web App | https://localhost:9090 | 111 | API Swagger | https://localhost:9090/swagger | 112 | Retrieve Token | https://localhost:9090/token | 113 | Logout | https://localhost:9090/logout?redirectUrl={your redirect url} | 114 | Show Claims | https://localhost:9090/whoami | 115 | Show Decrypted Session Cookie | https://localhost:9090/session | 116 | Gateway Health | https://localhost:9090/gateway/healthz | 117 | API Health | https://localhost:9090/api/healthz | 118 | 119 | ### User Credentials 120 | 121 | An Okta application has been configured for testing. The following users can be used to sign-in: 122 | 123 | Username | Password | Roles | 124 | --- | --- | --- | 125 | user@dev-28752567-admin.okta.com | Pa$$w0rd | Normal user | 126 | admin@dev-28752567-admin.okta.com | Pa$$w0rd | Can the /api/weatherforecast/detailed endpoint as an Admin | -------------------------------------------------------------------------------- /Weather.Api/Authentication/GatewayAuthenticationDefaults.cs: -------------------------------------------------------------------------------- 1 | namespace Weather.Api.Authentication 2 | { 3 | public static class GatewayAuthenticationDefaults 4 | { 5 | public const string AuthenticationScheme = "Gateway"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Weather.Api/Authentication/GatewayAuthenticationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | 3 | namespace Weather.Api.Authentication 4 | { 5 | public static class GatewayAuthenticationExtensions 6 | { 7 | public static AuthenticationBuilder AddGateway(this AuthenticationBuilder builder) 8 | { 9 | return builder.AddScheme(GatewayAuthenticationDefaults.AuthenticationScheme, options => { }); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Weather.Api/Authentication/GatewayAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.Net.Http.Headers; 5 | using System.Security.Claims; 6 | using System.Text.Encodings.Web; 7 | 8 | namespace Weather.Api.Authentication 9 | { 10 | public class GatewayAuthenticationHandler : AuthenticationHandler 11 | { 12 | public GatewayAuthenticationHandler( 13 | IOptionsMonitor options, 14 | ILoggerFactory logger, 15 | UrlEncoder encoder, 16 | ISystemClock clock) 17 | : base(options, logger, encoder, clock) 18 | { 19 | } 20 | 21 | protected override Task HandleAuthenticateAsync() 22 | { 23 | if (!Request.Headers.ContainsKey(HeaderNames.Authorization)) 24 | { 25 | return Task.FromResult(AuthenticateResult.Fail("Bearer token not found")); 26 | } 27 | 28 | if (!Request.Headers.TryGetValue(DefaultHeaders.Email, out var email)) 29 | { 30 | return Task.FromResult(AuthenticateResult.Fail("Forwarded identity headers not found")); 31 | } 32 | 33 | var claims = new[] 34 | { 35 | new Claim(ClaimTypes.NameIdentifier, email.ToString()), 36 | new Claim(ClaimTypes.Email, email.ToString()), 37 | new Claim(ClaimTypes.Name, Request.Headers[DefaultHeaders.Name]), 38 | new Claim(ClaimTypes.GivenName, Request.Headers[DefaultHeaders.GivenName]), 39 | new Claim(ClaimTypes.Surname, Request.Headers[DefaultHeaders.Surname]) 40 | }; 41 | 42 | var claimsIdentity = new ClaimsIdentity(claims, nameof(GatewayAuthenticationHandler), ClaimTypes.Email, ClaimTypes.Role); 43 | var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name); 44 | 45 | return Task.FromResult(AuthenticateResult.Success(ticket)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Weather.Api/Authentication/GatewayAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | 3 | namespace Weather.Api.Authentication 4 | { 5 | public class GatewayAuthenticationOptions : AuthenticationSchemeOptions 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Weather.Api/Authorization/AdminRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Weather.Api.Authorization 4 | { 5 | public class AdminRequirement : IAuthorizationRequirement 6 | { 7 | public readonly IReadOnlyList Administrators; 8 | 9 | public AdminRequirement(params string[] administrators) 10 | { 11 | Administrators = administrators; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Weather.Api/Authorization/AdminRequirementHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace Weather.Api.Authorization 4 | { 5 | public class AdminRequirementHandler : AuthorizationHandler 6 | { 7 | private readonly ILogger _logger; 8 | 9 | public AdminRequirementHandler(ILogger logger) 10 | { 11 | _logger = logger; 12 | } 13 | 14 | protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement) 15 | { 16 | var user = context.User.Identity.Name; 17 | if(string.IsNullOrEmpty(user)) 18 | { 19 | _logger.LogWarning("Forwarded identity headers not found"); 20 | context.Fail(); 21 | } 22 | var isAdmin = requirement.Administrators.Any(e => e.Equals(user, StringComparison.OrdinalIgnoreCase)); 23 | if (isAdmin) 24 | { 25 | context.Succeed(requirement); 26 | } 27 | else 28 | { 29 | _logger.LogWarning("{user} is not an admin", user); 30 | context.Fail(); 31 | } 32 | return Task.CompletedTask; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Weather.Api/Authorization/AuthorizationPolicies.cs: -------------------------------------------------------------------------------- 1 | namespace Weather.Api.Authorization 2 | { 3 | public static class AuthorizationPolicies 4 | { 5 | public const string Admin = "Admin"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Weather.Api/Controllers/AdminController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Weather.Api.Controllers 4 | { 5 | [ApiController] 6 | [Route("api/[controller]")] 7 | public class AdminController : ControllerBase 8 | { 9 | private static readonly string _secret = Guid.NewGuid().ToString(); 10 | 11 | private readonly ILogger _logger; 12 | 13 | public AdminController(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | [HttpGet("secret")] 19 | public IActionResult GetSecret() 20 | { 21 | return Ok(_secret); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Weather.Api/Controllers/DiagnosticsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Text; 3 | 4 | namespace Weather.Api.Controllers 5 | { 6 | [ApiController] 7 | [Route("api/[controller]")] 8 | public class DiagnosticsController : ControllerBase 9 | { 10 | [HttpGet] 11 | public IActionResult Get() 12 | { 13 | StringBuilder sb = new(); 14 | foreach (var header in Request.Headers) 15 | { 16 | sb.AppendLine($"{header.Key}: {header.Value.FirstOrDefault()}"); 17 | } 18 | 19 | return Ok(sb.ToString()); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Weather.Api/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Weather.Api.Authorization; 4 | using Common.Models; 5 | 6 | namespace Weather.Api.Controllers 7 | { 8 | [ApiController] 9 | [Route("api/[controller]")] 10 | public class WeatherForecastController : ControllerBase 11 | { 12 | private static readonly string[] Summaries = new[] 13 | { 14 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 15 | }; 16 | 17 | private readonly ILogger _logger; 18 | 19 | public WeatherForecastController(ILogger logger) 20 | { 21 | _logger = logger; 22 | } 23 | 24 | [HttpGet] 25 | public IActionResult Get() 26 | { 27 | var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 28 | { 29 | Date = DateTime.Now.AddDays(index), 30 | TemperatureC = Random.Shared.Next(-20, 55) 31 | }) 32 | .ToArray(); 33 | return Ok(forecasts); 34 | } 35 | 36 | [HttpGet("detailed")] 37 | [Authorize(Policy = AuthorizationPolicies.Admin)] 38 | public IActionResult GetDetailed() 39 | { 40 | var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 41 | { 42 | Date = DateTime.Now.AddDays(index), 43 | TemperatureC = Random.Shared.Next(-20, 55), 44 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 45 | }) 46 | .ToArray(); 47 | return Ok(forecasts); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Weather.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base 2 | WORKDIR /app 3 | EXPOSE 8080 4 | 5 | ENV ASPNETCORE_URLS=http://+:8080 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build 8 | 9 | COPY [".", "/src/"] 10 | 11 | WORKDIR src/Weather.Api 12 | RUN dotnet restore "Weather.Api.csproj" 13 | 14 | RUN dotnet publish "Weather.Api.csproj" --no-restore -c Release -o /app/publish 15 | 16 | FROM base AS final 17 | WORKDIR /app 18 | COPY --from=build /app/publish . 19 | USER 1000 20 | ENTRYPOINT ["dotnet", "Weather.Api.dll"] -------------------------------------------------------------------------------- /Weather.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using HealthChecks.UI.Client; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 4 | using Weather.Api.Authentication; 5 | using Weather.Api.Authorization; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | // Add services to the container. 10 | 11 | builder.Services.AddControllers(); 12 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 13 | builder.Services.AddEndpointsApiExplorer(); 14 | builder.Services.AddSwaggerGen(); 15 | builder.Services.AddHttpContextAccessor(); 16 | 17 | builder.Services.AddAuthentication(GatewayAuthenticationDefaults.AuthenticationScheme) 18 | .AddGateway(); 19 | 20 | builder.Services.AddTransient(); 21 | builder.Services.AddAuthorization(options => 22 | { 23 | options.FallbackPolicy = new AuthorizationPolicyBuilder() 24 | .RequireAuthenticatedUser() 25 | .Build(); 26 | options.AddPolicy(AuthorizationPolicies.Admin, new AuthorizationPolicyBuilder() 27 | .RequireAuthenticatedUser() 28 | .AddRequirements(new AdminRequirement("admin@dev-28752567-admin.okta.com")) 29 | .Build()); 30 | }); 31 | 32 | builder.Services.AddHealthChecks() 33 | .AddLiveness("api"); 34 | 35 | var app = builder.Build(); 36 | 37 | app.UseSwagger(); 38 | app.UseSwaggerUI(); 39 | 40 | app.UseAuthentication(); 41 | app.UseAuthorization(); 42 | 43 | app.MapControllers(); 44 | app.MapHealthChecks("/api/liveness", new HealthCheckOptions 45 | { 46 | Predicate = r => r.Name.Contains("self") 47 | }).AllowAnonymous(); 48 | app.MapHealthChecks("/api/healthz", new HealthCheckOptions() 49 | { 50 | Predicate = _ => true, 51 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 52 | }).AllowAnonymous(); 53 | 54 | app.Run(); 55 | -------------------------------------------------------------------------------- /Weather.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "Weather.Api": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "https://localhost:7149;http://localhost:5149", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Weather.Api/Weather.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Weather.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Weather.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Weather.Web/Client/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /Weather.Web/Client/Pages/AccessDenied.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Index 4 | 5 |

Hello, world!

6 | 7 | Welcome to your new app. 8 | 9 | 10 | -------------------------------------------------------------------------------- /Weather.Web/Client/Pages/FetchData.razor: -------------------------------------------------------------------------------- 1 | @page "/fetchdata" 2 | @using Common.Models 3 | @inject HttpClient Http 4 | 5 | Weather forecast 6 | 7 |

Weather forecast

8 | 9 |

This component demonstrates fetching data from the server.

10 | 11 | @if (forecasts == null) 12 | { 13 |

Loading...

14 | } 15 | else 16 | { 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @foreach (var forecast in forecasts) 28 | { 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 36 | 37 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
38 | } 39 | 40 | @code { 41 | private WeatherForecast[]? forecasts; 42 | 43 | protected override async Task OnInitializedAsync() 44 | { 45 | forecasts = await Http.GetFromJsonAsync("WeatherForecast"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Weather.Web/Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/AccessDenied" 2 | 3 | Forbidden 4 | 5 |

Forbidden

6 | 7 | You are not authorized to view this. 8 | -------------------------------------------------------------------------------- /Weather.Web/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Web; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | using Weather.Web.Client; 5 | 6 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 7 | builder.RootComponents.Add("#app"); 8 | builder.RootComponents.Add("head::after"); 9 | 10 | builder.Services.AddScoped(serviceProvider => 11 | { 12 | var navigationManager = serviceProvider.GetRequiredService(); 13 | return new HttpClient { BaseAddress = new Uri($"{navigationManager.BaseUri}api/") }; 14 | }); 15 | 16 | await builder.Build().RunAsync(); 17 | -------------------------------------------------------------------------------- /Weather.Web/Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Weather.Web": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": true, 7 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 8 | "applicationUrl": "https://localhost:7022;http://localhost:5022", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Weather.Web/Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /Weather.Web/Client/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row:not(.auth) { 41 | display: none; 42 | } 43 | 44 | .top-row.auth { 45 | justify-content: space-between; 46 | } 47 | 48 | .top-row ::deep a, .top-row ::deep .btn-link { 49 | margin-left: 0; 50 | } 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .page { 55 | flex-direction: row; 56 | } 57 | 58 | .sidebar { 59 | width: 250px; 60 | height: 100vh; 61 | position: sticky; 62 | top: 0; 63 | } 64 | 65 | .top-row { 66 | position: sticky; 67 | top: 0; 68 | z-index: 1; 69 | } 70 | 71 | .top-row.auth ::deep a:first-child { 72 | flex: 1; 73 | text-align: right; 74 | width: 0; 75 | } 76 | 77 | .top-row, article { 78 | padding-left: 2rem !important; 79 | padding-right: 1.5rem !important; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Weather.Web/Client/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  9 | 10 |
11 | 23 |
24 | 25 | @code { 26 | private bool collapseNavMenu = true; 27 | 28 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 29 | 30 | private void ToggleNavMenu() 31 | { 32 | collapseNavMenu = !collapseNavMenu; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Weather.Web/Client/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Weather.Web/Client/Shared/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 | 
2 | 3 | @Title 4 | 5 | 6 | Please take our 7 | brief survey 8 | 9 | and tell us what you think. 10 |
11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string? Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /Weather.Web/Client/Weather.Web.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Weather.Web/Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using Weather.Web.Client 10 | @using Weather.Web.Client.Shared 11 | -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | h1:focus { 8 | outline: none; 9 | } 10 | 11 | a, .btn-link { 12 | color: #0071c1; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .content { 22 | padding-top: 1.1rem; 23 | } 24 | 25 | .valid.modified:not([type=checkbox]) { 26 | outline: 1px solid #26b050; 27 | } 28 | 29 | .invalid { 30 | outline: 1px solid red; 31 | } 32 | 33 | .validation-message { 34 | color: red; 35 | } 36 | 37 | #blazor-error-ui { 38 | background: lightyellow; 39 | bottom: 0; 40 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 41 | display: none; 42 | left: 0; 43 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 44 | position: fixed; 45 | width: 100%; 46 | z-index: 1000; 47 | } 48 | 49 | #blazor-error-ui .dismiss { 50 | cursor: pointer; 51 | position: absolute; 52 | right: 0.75rem; 53 | top: 0.5rem; 54 | } 55 | 56 | .blazor-error-boundary { 57 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 58 | padding: 1rem 1rem 1rem 3.7rem; 59 | color: white; 60 | } 61 | 62 | .blazor-error-boundary::after { 63 | content: "An error has occurred." 64 | } 65 | -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-bentley/AuthenticationProxy/2840a4abdcc07e91a328dec493aafa758b600d42/Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-bentley/AuthenticationProxy/2840a4abdcc07e91a328dec493aafa758b600d42/Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014 9 | By P.J. Onori 10 | Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) 11 | 12 | 13 | 14 | 27 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 71 | 74 | 76 | 79 | 81 | 84 | 86 | 88 | 91 | 93 | 95 | 98 | 100 | 102 | 104 | 106 | 109 | 112 | 115 | 117 | 121 | 123 | 125 | 127 | 130 | 132 | 134 | 136 | 138 | 141 | 143 | 145 | 147 | 149 | 151 | 153 | 155 | 157 | 159 | 162 | 165 | 167 | 169 | 172 | 174 | 177 | 179 | 181 | 183 | 185 | 189 | 191 | 194 | 196 | 198 | 200 | 202 | 205 | 207 | 209 | 211 | 213 | 215 | 218 | 220 | 222 | 224 | 226 | 228 | 230 | 232 | 234 | 236 | 238 | 241 | 243 | 245 | 247 | 249 | 251 | 253 | 256 | 259 | 261 | 263 | 265 | 267 | 269 | 272 | 274 | 276 | 280 | 282 | 285 | 287 | 289 | 292 | 295 | 298 | 300 | 302 | 304 | 306 | 309 | 312 | 314 | 316 | 318 | 320 | 322 | 324 | 326 | 330 | 334 | 338 | 340 | 343 | 345 | 347 | 349 | 351 | 353 | 355 | 358 | 360 | 363 | 365 | 367 | 369 | 371 | 373 | 375 | 377 | 379 | 381 | 383 | 386 | 388 | 390 | 392 | 394 | 396 | 399 | 401 | 404 | 406 | 408 | 410 | 412 | 414 | 416 | 419 | 421 | 423 | 425 | 428 | 431 | 435 | 438 | 440 | 442 | 444 | 446 | 448 | 451 | 453 | 455 | 457 | 460 | 462 | 464 | 466 | 468 | 471 | 473 | 477 | 479 | 481 | 483 | 486 | 488 | 490 | 492 | 494 | 496 | 499 | 501 | 504 | 506 | 509 | 512 | 515 | 517 | 520 | 522 | 524 | 526 | 529 | 532 | 534 | 536 | 539 | 542 | 543 | 544 | -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-bentley/AuthenticationProxy/2840a4abdcc07e91a328dec493aafa758b600d42/Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-bentley/AuthenticationProxy/2840a4abdcc07e91a328dec493aafa758b600d42/Weather.Web/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-bentley/AuthenticationProxy/2840a4abdcc07e91a328dec493aafa758b600d42/Weather.Web/Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matt-bentley/AuthenticationProxy/2840a4abdcc07e91a328dec493aafa758b600d42/Weather.Web/Client/wwwroot/icon-192.png -------------------------------------------------------------------------------- /Weather.Web/Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Weather.Web 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Loading...
16 | 17 |
18 | An unhandled error has occurred. 19 | Reload 20 | 🗙 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Weather.Web/Server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base 2 | WORKDIR /app 3 | EXPOSE 8080 4 | 5 | ENV ASPNETCORE_URLS=http://+:8080 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build 8 | 9 | COPY [".", "/src/"] 10 | 11 | WORKDIR src/Weather.Web/Server 12 | RUN dotnet restore "Weather.Web.Server.csproj" 13 | 14 | RUN dotnet publish "Weather.Web.Server.csproj" --no-restore -c Release -o /app/publish 15 | 16 | FROM base AS final 17 | WORKDIR /app 18 | COPY --from=build /app/publish . 19 | USER 1000 20 | ENTRYPOINT ["dotnet", "Weather.Web.Server.dll"] -------------------------------------------------------------------------------- /Weather.Web/Server/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Weather.Web.Server.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Weather.Web/Server/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | 5 | namespace Weather.Web.Server.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Weather.Web/Server/Program.cs: -------------------------------------------------------------------------------- 1 | 2 | var builder = WebApplication.CreateBuilder(args); 3 | 4 | // Add services to the container. 5 | 6 | builder.Services.AddRazorPages(); 7 | 8 | var app = builder.Build(); 9 | 10 | // Configure the HTTP request pipeline. 11 | if (app.Environment.IsDevelopment()) 12 | { 13 | app.UseWebAssemblyDebugging(); 14 | } 15 | else 16 | { 17 | app.UseExceptionHandler("/Error"); 18 | } 19 | 20 | app.UseBlazorFrameworkFiles(); 21 | app.UseStaticFiles(); 22 | 23 | app.UseRouting(); 24 | 25 | 26 | app.MapRazorPages(); 27 | app.MapControllers(); 28 | app.MapFallbackToFile("index.html"); 29 | 30 | app.Run(); 31 | -------------------------------------------------------------------------------- /Weather.Web/Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Weather.Web.Server": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": true, 6 | "launchBrowser": false, 7 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 8 | "applicationUrl": "https://localhost:7022;http://localhost:5022", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Weather.Web/Server/Weather.Web.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Weather.Web/Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Weather.Web/Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /YarpSecurity.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32421.90 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{627F6683-B85A-4E03-9773-496DE7BA1CF6}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Gateway", "Gateway\Gateway.csproj", "{2E42933C-F42B-485E-9B46-9AAA4EBAA34C}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weather.Api", "Weather.Api\Weather.Api.csproj", "{45A35464-683E-4271-94A7-4FAC1709B869}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weather.Web.Server", "Weather.Web\Server\Weather.Web.Server.csproj", "{DD445F9A-6820-4F1B-85A9-245950A4A1E2}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Weather.Web.Client", "Weather.Web\Client\Weather.Web.Client.csproj", "{3C358948-3C0D-42E0-A455-227D163D9F40}" 15 | EndProject 16 | Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{345E100C-C092-48A6-8134-18CB1548E995}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_SolutionItems", "_SolutionItems", "{32C6F2A6-5BA0-48F8-BC7E-81949C113BDB}" 19 | ProjectSection(SolutionItems) = preProject 20 | Gateway\aspnetapp.crt = Gateway\aspnetapp.crt 21 | README.md = README.md 22 | EndProjectSection 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Weather.Web", "Weather.Web", "{DA8973B7-19B6-4544-BDFE-EFECA8BD48AB}" 25 | EndProject 26 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "k8s", "k8s\k8s.shproj", "{B2301745-2A0E-4E45-A8F7-0401B59C91FC}" 27 | EndProject 28 | Global 29 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 30 | k8s\k8s.projitems*{b2301745-2a0e-4e45-a8f7-0401b59c91fc}*SharedItemsImports = 13 31 | EndGlobalSection 32 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 33 | Debug|Any CPU = Debug|Any CPU 34 | Release|Any CPU = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {627F6683-B85A-4E03-9773-496DE7BA1CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {627F6683-B85A-4E03-9773-496DE7BA1CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {627F6683-B85A-4E03-9773-496DE7BA1CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {627F6683-B85A-4E03-9773-496DE7BA1CF6}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {2E42933C-F42B-485E-9B46-9AAA4EBAA34C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {2E42933C-F42B-485E-9B46-9AAA4EBAA34C}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {2E42933C-F42B-485E-9B46-9AAA4EBAA34C}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {2E42933C-F42B-485E-9B46-9AAA4EBAA34C}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {45A35464-683E-4271-94A7-4FAC1709B869}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {45A35464-683E-4271-94A7-4FAC1709B869}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {45A35464-683E-4271-94A7-4FAC1709B869}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {45A35464-683E-4271-94A7-4FAC1709B869}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {DD445F9A-6820-4F1B-85A9-245950A4A1E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {DD445F9A-6820-4F1B-85A9-245950A4A1E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {DD445F9A-6820-4F1B-85A9-245950A4A1E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {DD445F9A-6820-4F1B-85A9-245950A4A1E2}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {3C358948-3C0D-42E0-A455-227D163D9F40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {3C358948-3C0D-42E0-A455-227D163D9F40}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {3C358948-3C0D-42E0-A455-227D163D9F40}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {3C358948-3C0D-42E0-A455-227D163D9F40}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {345E100C-C092-48A6-8134-18CB1548E995}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {345E100C-C092-48A6-8134-18CB1548E995}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | EndGlobalSection 60 | GlobalSection(SolutionProperties) = preSolution 61 | HideSolutionNode = FALSE 62 | EndGlobalSection 63 | GlobalSection(NestedProjects) = preSolution 64 | {DD445F9A-6820-4F1B-85A9-245950A4A1E2} = {DA8973B7-19B6-4544-BDFE-EFECA8BD48AB} 65 | {3C358948-3C0D-42E0-A455-227D163D9F40} = {DA8973B7-19B6-4544-BDFE-EFECA8BD48AB} 66 | EndGlobalSection 67 | GlobalSection(ExtensibilityGlobals) = postSolution 68 | SolutionGuid = {0C7709C6-5C56-47AA-A244-8F87280A885B} 69 | EndGlobalSection 70 | EndGlobal 71 | -------------------------------------------------------------------------------- /YarpSecurity.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /aspnetapp.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDDTCCAfWgAwIBAgIJAJeOjRiL5W7sMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV 3 | BAMTCWxvY2FsaG9zdDAeFw0yMjA5MDYxODM2MTJaFw0yMzA5MDYxODM2MTJaMBQx 4 | EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAMzVTYnrCCL0E1KoOZDAirfE0uZx7Mpr5QqaEDNQjdLc1BWnpdQQhRW/YPIB 6 | rQWtmg0vMAQ//Qu03fxwmLFLd2jmnGFFKU20BV3C0wtaJOdVo2/WikfpAtPKyr2N 7 | quKpX+qO1bEcgTdzE9dGYz0PiJazlnqVirqmTw1YU5QIgcqgvNHY91klK5Gr4L69 8 | tmk5c/ds2xkuNqVJmQeYUBmPRydA4miJyKlO0V/VIlZ9ilD0Z25CGlX61Hjb5qgj 9 | ecVA1LxztotGlebRWaET7APU7SUeAC6SH7V+2IdjiwrIzgrQ/ZLewaBaOLqYRtUr 10 | qAD4qkSjXpOh57VS26vQpAxN/TUCAwEAAaNiMGAwDAYDVR0TAQH/BAIwADAOBgNV 11 | HQ8BAf8EBAMCBaAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwFwYDVR0RAQH/BA0w 12 | C4IJbG9jYWxob3N0MA8GCisGAQQBgjdUAQEEAQIwDQYJKoZIhvcNAQELBQADggEB 13 | AEsNwQrigdo+V7EZagboeOmsgJU1dzgDmlDuQX/rjZn7ryXAEanJTweohRvjeExY 14 | 5qUlqRKvnjJdtvs44CBtKUjo6pfVhd4ON0rxR3jklusPbH/oeCP8JUjYO25JpzI8 15 | 6sD6ftFR2wa7hqIEThUW+Ixkz3RKzYRYTtz/SL+xfVGmaeKo2Z4KdGmbu4o4zHJU 16 | WL/lrxtswDOX6V3W3yKXbvdAhTzOMl/dbNdPJn5i4Lr3+oQeRCXZislFj8Ocy1Yt 17 | 99q/ussdmyvJYQbYRLMCsyZUQey3Xe2u2dEYBkARnLQIAFrhjbF5FtU/7QEUJ61X 18 | jofKYCS4YAfb+lFSbWHReL4= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | 345e100c-c092-48a6-8134-18cb1548e995 7 | LaunchBrowser 8 | {Scheme}://localhost:{ServicePort} 9 | puck.vat.filing.web 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | 4 | gateway: 5 | build: 6 | context: . 7 | dockerfile: Gateway/Dockerfile 8 | image: yarp/security/gateway:dev 9 | 10 | api: 11 | build: 12 | context: . 13 | dockerfile: Weather.Api/Dockerfile 14 | image: yarp/security/api:dev 15 | 16 | web: 17 | build: 18 | context: . 19 | dockerfile: Weather.Web/Server/Dockerfile 20 | image: yarp/security/web:dev -------------------------------------------------------------------------------- /k8s/api/api.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: api 6 | name: api 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: api 12 | template: 13 | metadata: 14 | labels: 15 | app: api 16 | spec: 17 | containers: 18 | - image: yarp/security/api:dev 19 | name: api 20 | readinessProbe: 21 | httpGet: 22 | path: /api/liveness 23 | port: http 24 | periodSeconds: 30 25 | livenessProbe: 26 | httpGet: 27 | path: /api/liveness 28 | port: http 29 | periodSeconds: 30 30 | failureThreshold: 10 31 | startupProbe: 32 | httpGet: 33 | path: /api/liveness 34 | port: http 35 | failureThreshold: 60 36 | periodSeconds: 2 37 | securityContext: 38 | privileged: false 39 | allowPrivilegeEscalation: false 40 | ports: 41 | - name: http 42 | containerPort: 8080 43 | - image: yarp/security/gateway:dev 44 | name: gateway 45 | env: 46 | - name: IdentityServer__RedirectUri 47 | value: "https://localhost/signin-oidc" 48 | volumeMounts: 49 | - name: "gateway-config" 50 | mountPath: "/app/appsettings.Production.json" 51 | subPath: "appsettings.Production.json" 52 | readinessProbe: 53 | httpGet: 54 | path: /gateway/liveness 55 | port: http 56 | periodSeconds: 30 57 | livenessProbe: 58 | httpGet: 59 | path: /gateway/liveness 60 | port: http 61 | periodSeconds: 30 62 | failureThreshold: 10 63 | startupProbe: 64 | httpGet: 65 | path: /gateway/liveness 66 | port: http 67 | failureThreshold: 60 68 | periodSeconds: 2 69 | securityContext: 70 | privileged: false 71 | allowPrivilegeEscalation: false 72 | ports: 73 | - name: http 74 | containerPort: 8081 75 | restartPolicy: Always 76 | volumes: 77 | - name: "gateway-config" 78 | configMap: 79 | name: "api-gateway-config" 80 | --- 81 | apiVersion: v1 82 | kind: Service 83 | metadata: 84 | name: api 85 | labels: 86 | app: api 87 | spec: 88 | type: "ClusterIP" 89 | ports: 90 | - name: http 91 | port: 80 92 | protocol: TCP 93 | targetPort: 8081 94 | selector: 95 | app: api 96 | --- 97 | apiVersion: v1 98 | kind: ConfigMap 99 | metadata: 100 | name: api-gateway-config 101 | data: 102 | appsettings.Production.json: | 103 | { 104 | "ReverseProxy": { 105 | "Routes": { 106 | "healthz": { 107 | "ClusterId": "downstream", 108 | "AuthorizationPolicy": "anonymous", 109 | "Match": { 110 | "Path": "/api/healthz" 111 | } 112 | }, 113 | "admin": { 114 | "ClusterId": "downstream", 115 | "AuthorizationPolicy": "admin", 116 | "Match": { 117 | "Path": "/api/admin/{**catch-all}" 118 | } 119 | }, 120 | "downstream": { 121 | "ClusterId": "downstream", 122 | "AuthorizationPolicy": "default", 123 | "Match": { 124 | "Path": "/{**catch-all}" 125 | } 126 | } 127 | }, 128 | "Clusters": { 129 | "downstream": { 130 | "Destinations": { 131 | "downstream-1": { 132 | "Address": "http://localhost:8080/" 133 | } 134 | } 135 | } 136 | } 137 | }, 138 | "Administrators": [ 139 | "admin@dev-28752567-admin.okta.com" 140 | ] 141 | } -------------------------------------------------------------------------------- /k8s/ingress/README.md: -------------------------------------------------------------------------------- 1 | # Ingress Deployment 2 | 3 | The nginx ingress controller is used to serve traffic to the namespace. 4 | 5 | The controller can be deployed using: 6 | ```bash 7 | helm install nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginx 8 | ``` 9 | 10 | ## Generate Certificate Secret 11 | 12 | A TLS secret must be created using the self-signed pfx SSL certificate using the following: 13 | 14 | ```bash 15 | # this must be executed from the Gateway/Certificates folder 16 | # extract encrypted private key from pfx 17 | openssl pkcs12 -in aspnetapp.pfx -nocerts -out aspnetapp.key 18 | 19 | # the password for the provided cert is 'password' 20 | 21 | # extract the public key 22 | openssl pkcs12 -in aspnetapp.pfx -clcerts -nokeys -out aspnetapp.crt 23 | 24 | # decrypt the private key 25 | openssl rsa -in aspnetapp.key -out aspnetapp-decrypted.key 26 | ``` 27 | 28 | Create the kubernetes TLS secret: 29 | 30 | ```bash 31 | kubectl create secret tls tls-certificate --key="aspnetapp-decrypted.key" --cert="aspnetapp.crt" --dry-run=client -o yaml 32 | ``` 33 | 34 | ## Generating a new Certificate 35 | 36 | If a new pfx SSL is needed then dotnet dev-certs can be created using the following: 37 | ```cmd 38 | dotnet dev-certs https --clean 39 | dotnet dev-certs https -ep %USERPROFILE%\.aspnet\https\aspnetapp.pfx -p password 40 | dotnet dev-certs https --trust 41 | ``` 42 | 43 | The new certificate must be added to the `Gateway` project. -------------------------------------------------------------------------------- /k8s/ingress/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | creationTimestamp: null 5 | name: tls-certificate 6 | type: kubernetes.io/tls 7 | data: 8 | tls.crt: QmFnIEF0dHJpYnV0ZXMNCiAgICBsb2NhbEtleUlEOiAwMSAwMCAwMCAwMCANCiAgICBmcmllbmRseU5hbWU6IEFTUC5ORVQgQ29yZSBIVFRQUyBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZQ0Kc3ViamVjdD1DTiA9IGxvY2FsaG9zdA0KaXNzdWVyPUNOID0gbG9jYWxob3N0DQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0NCk1JSUREVENDQWZXZ0F3SUJBZ0lKQUplT2pSaUw1VzdzTUEwR0NTcUdTSWIzRFFFQkN3VUFNQlF4RWpBUUJnTlYNCkJBTVRDV3h2WTJGc2FHOXpkREFlRncweU1qQTVNRFl4T0RNMk1USmFGdzB5TXpBNU1EWXhPRE0yTVRKYU1CUXgNCkVqQVFCZ05WQkFNVENXeHZZMkZzYUc5emREQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0MNCmdnRUJBTXpWVFluckNDTDBFMUtvT1pEQWlyZkUwdVp4N01wcjVRcWFFRE5RamRMYzFCV25wZFFRaFJXL1lQSUINCnJRV3RtZzB2TUFRLy9RdTAzZnh3bUxGTGQyam1uR0ZGS1UyMEJWM0Mwd3RhSk9kVm8yL1dpa2ZwQXRQS3lyMk4NCnF1S3BYK3FPMWJFY2dUZHpFOWRHWXowUGlKYXpsbnFWaXJxbVR3MVlVNVFJZ2NxZ3ZOSFk5MWtsSzVHcjRMNjkNCnRtazVjL2RzMnhrdU5xVkptUWVZVUJtUFJ5ZEE0bWlKeUtsTzBWL1ZJbFo5aWxEMFoyNUNHbFg2MUhqYjVxZ2oNCmVjVkExTHh6dG90R2xlYlJXYUVUN0FQVTdTVWVBQzZTSDdWKzJJZGppd3JJemdyUS9aTGV3YUJhT0xxWVJ0VXINCnFBRDRxa1NqWHBPaDU3VlMyNnZRcEF4Ti9UVUNBd0VBQWFOaU1HQXdEQVlEVlIwVEFRSC9CQUl3QURBT0JnTlYNCkhROEJBZjhFQkFNQ0JhQXdGZ1lEVlIwbEFRSC9CQXd3Q2dZSUt3WUJCUVVIQXdFd0Z3WURWUjBSQVFIL0JBMHcNCkM0SUpiRzlqWVd4b2IzTjBNQThHQ2lzR0FRUUJnamRVQVFFRUFRSXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUINCkFFc053UXJpZ2RvK1Y3RVphZ2JvZU9tc2dKVTFkemdEbWxEdVFYL3JqWm43cnlYQUVhbkpUd2VvaFJ2amVFeFkNCjVxVWxxUkt2bmpKZHR2czQ0Q0J0S1VqbzZwZlZoZDRPTjByeFIzamtsdXNQYkgvb2VDUDhKVWpZTzI1SnB6STgNCjZzRDZmdEZSMndhN2hxSUVUaFVXK0l4a3ozUkt6WVJZVHR6L1NMK3hmVkdtYWVLbzJaNEtkR21idTRvNHpISlUNCldML2xyeHRzd0RPWDZWM1czeUtYYnZkQWhUek9NbC9kYk5kUEpuNWk0THIzK29RZVJDWFppc2xGajhPY3kxWXQNCjk5cS91c3NkbXl2SllRYllSTE1Dc3laVVFleTNYZTJ1MmRFWUJrQVJuTFFJQUZyaGpiRjVGdFUvN1FFVUo2MVgNCmpvZktZQ1M0WUFmYitsRlNiV0hSZUw0PQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0K 9 | tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tDQpNSUlFdmdJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLZ3dnZ1NrQWdFQUFvSUJBUURNMVUySjZ3Z2k5Qk5TDQpxRG1Rd0lxM3hOTG1jZXpLYStVS21oQXpVSTNTM05RVnA2WFVFSVVWdjJEeUFhMEZyWm9OTHpBRVAvMEx0TjM4DQpjSml4UzNkbzVweGhSU2xOdEFWZHd0TUxXaVRuVmFOdjFvcEg2UUxUeXNxOWphcmlxVi9xanRXeEhJRTNjeFBYDQpSbU05RDRpV3M1WjZsWXE2cGs4TldGT1VDSUhLb0x6UjJQZFpKU3VScStDK3ZiWnBPWFAzYk5zWkxqYWxTWmtIDQptRkFaajBjblFPSm9pY2lwVHRGZjFTSldmWXBROUdkdVFocFYrdFI0Mithb0kzbkZRTlM4YzdhTFJwWG0wVm1oDQpFK3dEMU8wbEhnQXVraCsxZnRpSFk0c0t5TTRLMFAyUzNzR2dXamk2bUViVks2Z0ErS3BFbzE2VG9lZTFVdHVyDQowS1FNVGYwMUFnTUJBQUVDZ2dFQURhcnArcHJ4Tnd2Y1FqYXhhS1JBWFZUSDBkS21QemVGMS9oQnZuclFpOUpxDQpZR3VJV0NjbUNqWno3SklLRG9mdjhKTkdlL0lkRzJ2ZEtCd0hSRVZKMlhrUjFEZStNczRoVVlsUGRjRjFGdTZTDQo4a0o1aGFUWnFISXVwd2xON3NQb2Y0azZYeEFNMnp0VmhwYk94bU5EOVBKVXh5YzFVNUVIVTFlaWRBMUJROWpwDQpTVEJwR0NCdG5DNThHRUpMcnZHUVI3dllUYXl2NEx6amZDSGtaZHYrV1Z3R0VjemRobXh3dk40Sm9CeXJpOG1IDQp1UmFyL2xxNFpQa1RvUGI0dGovWmIyQm9EM3p6bTdGcldydjVpb2FJUE55anJtMjZYVlFqMmc1eThzMHFXOEJKDQpDdnBqYW5LcVplSlM5Nzk5bVZzU0JsRVA0RzJoc0JTNEVIT09BVnNSdlFLQmdRRHZtcjVjZUhmTlNMMTFsSjRtDQpCNnJDc1FoSkdSVE1rd0NQYW9VaUZMc1k3bGZFRUFwYmswRHVLd25MZUY2aHJ3ek9WakhmRjdaMmJVbDkydXhvDQpRampZQjBpcjBaMnNXZ1pWM2EzdnRZTXFXb2h5UlJlT25iS0VpbGpPV2E0NnhlRkdsWnUvRVp0dHRMdEZvOVlsDQpzYVZWN3RvOFdmNEdtak9rRjZGZzRZek9wd0tCZ1FEYTJYUzkybzd4UHVwTjk3SlVhOG1oVlpzdjJ1MGYvZnczDQoraE1YRmRkalcxNlYxcGxYTzhHM1YwbkNWeHVYaHJ5M1V0REVvelZsUTRKemRDT1gyY0dVcXNIUmEzTEJsbFZuDQpLQy9ScUtUWU1iMmZoMllIN1k5K1RWZTM0SFQwQ3VKd0k2T3lYYTdoQUkxbHd4TUxiQWVzRU1LOXpHNXNyMFA0DQo3aEZET3hWTXd3S0JnUUNaWHJZSFB0MkF1OVQ2VkowK3ZwMmhiZXhkemY0d1ROQ0kvS2duZnBtY2hjUU1MakJUDQpHZGp3R3dCMEM2S1hZQk9VYUR3WUQzZXVLcHBtUXlJY1FuZGNZSjAxemtGZE4ycml2N2s4dlRVdWdURGg1RFFHDQpDQTlqSTZOZ3dxcngyQVF4U0VhZmNVZktXMnVjVWhrYm03QzNadUt1eXZQOWllc1FXRWZtV2JubDlRS0JnRWNaDQoxeC9IWGlGcXUxelJXSktzNGprVVF2THRRWlRmUTQ0OEpPK2ZIUGNldkVBYUZIK08zb1RlU3d4a21JWkdOQkdyDQpTa0JZZGdTTVJsbVVKZXd6eHZ5eDdYNUlWbVFtbjJDQVllalB6WjZmcW1lbDZiUXhyaWp1bjgwUmsyMm1DNk03DQpNUGZHRUFieDB2NTR2anM3d2RKWkw2dlNPeDM4WXkwQXk0TDRNeGRaQW9HQkFLNFpXTmI5VlZFRHJIZlp5RlN1DQpvNzRwNVlyTUV0d1JvSzNFM3dGUEswRWt6S0JoOVk2djkxKy9OTDlHSU45cUdzZFltTjhzbExVcUtQNFJ4aFEyDQowcjBybE9MdW5CMjFnQWs0OWM3ZDUwb1QzY2kxelk2T0RuVS9NbWUwWEJGMlJXY1EzVExrWkQ0ZlgwU3NnL1BpDQpxQnhYQXhzUlpENS8xbFdsVmhHZCtwVSsNCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0NCg== 10 | 11 | --- 12 | apiVersion: networking.k8s.io/v1 13 | kind: Ingress 14 | metadata: 15 | name: nginx-ingress 16 | annotations: 17 | nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" # this is needed to accept the OIDC cookie from the IDP 18 | spec: 19 | ingressClassName: nginx 20 | rules: 21 | - host: localhost 22 | http: 23 | paths: 24 | - pathType: Prefix 25 | backend: 26 | service: 27 | name: api 28 | port: 29 | number: 80 30 | path: /api/ 31 | - pathType: Prefix 32 | backend: 33 | service: 34 | name: api 35 | port: 36 | number: 80 37 | path: /swagger 38 | - pathType: Prefix 39 | backend: 40 | service: 41 | name: web 42 | port: 43 | number: 80 44 | path: / 45 | tls: 46 | - hosts: 47 | - localhost 48 | secretName: tls-certificate -------------------------------------------------------------------------------- /k8s/k8s.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | b2301745-2a0e-4e45-a8f7-0401b59c91fc 7 | 8 | 9 | k8s 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /k8s/k8s.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | b2301745-2a0e-4e45-a8f7-0401b59c91fc 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /k8s/web/web.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: web 6 | name: web 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: web 12 | template: 13 | metadata: 14 | labels: 15 | app: web 16 | spec: 17 | containers: 18 | - image: yarp/security/web:dev 19 | name: web 20 | securityContext: 21 | privileged: false 22 | allowPrivilegeEscalation: false 23 | ports: 24 | - name: http 25 | containerPort: 8080 26 | - image: yarp/security/gateway:dev 27 | name: gateway 28 | env: 29 | - name: IdentityServer__RedirectUri 30 | value: "https://localhost/signin-oidc" 31 | readinessProbe: 32 | httpGet: 33 | path: /gateway/liveness 34 | port: http 35 | periodSeconds: 30 36 | livenessProbe: 37 | httpGet: 38 | path: /gateway/liveness 39 | port: http 40 | periodSeconds: 30 41 | failureThreshold: 10 42 | startupProbe: 43 | httpGet: 44 | path: /gateway/liveness 45 | port: http 46 | failureThreshold: 60 47 | periodSeconds: 2 48 | securityContext: 49 | privileged: false 50 | allowPrivilegeEscalation: false 51 | ports: 52 | - name: http 53 | containerPort: 8081 54 | --- 55 | apiVersion: v1 56 | kind: Service 57 | metadata: 58 | name: web 59 | labels: 60 | app: web 61 | spec: 62 | type: "ClusterIP" 63 | ports: 64 | - name: http 65 | port: 80 66 | protocol: TCP 67 | targetPort: 8081 68 | selector: 69 | app: web --------------------------------------------------------------------------------