├── .gitattributes ├── .gitignore ├── .idea └── .idea.ASPNETReact │ └── .idea │ ├── .gitignore │ ├── encodings.xml │ ├── indexLayout.xml │ └── vcs.xml ├── ASPNETReact.csproj ├── ASPNETReact.sln ├── ClientApp ├── .env ├── .env.development ├── .gitignore ├── README.md ├── aspnetcore-https.js ├── aspnetcore-react.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src │ ├── App.js │ ├── App.test.js │ ├── components │ ├── Counter.js │ ├── FetchData.js │ ├── Home.js │ ├── Layout.js │ ├── NavMenu.css │ ├── NavMenu.js │ └── api-authorization │ │ ├── ApiAuthorizationConstants.js │ │ ├── ApiAuthorizationRoutes.js │ │ ├── AuthorizeRoute.js │ │ ├── AuthorizeService.js │ │ ├── Login.js │ │ ├── LoginMenu.js │ │ └── Logout.js │ ├── custom.css │ ├── index.js │ ├── reportWebVitals.js │ ├── service-worker.js │ ├── serviceWorkerRegistration.js │ ├── setupProxy.js │ └── setupTests.js ├── Controllers ├── OidcConfigurationController.cs └── WeatherForecastController.cs ├── Data ├── ApplicationDbContext.cs └── Migrations │ ├── 00000000000000_CreateIdentitySchema.Designer.cs │ ├── 00000000000000_CreateIdentitySchema.cs │ └── ApplicationDbContextModelSnapshot.cs ├── Models └── ApplicationUser.cs ├── Pages ├── Error.cshtml ├── Error.cshtml.cs ├── Shared │ └── _LoginPartial.cshtml └── _ViewImports.cshtml ├── Program.cs ├── Properties ├── launchSettings.json ├── serviceDependencies.json └── serviceDependencies.local.json ├── WeatherForecast.cs ├── appsettings.Development.json ├── appsettings.json └── package-lock.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | bin/ 23 | Bin/ 24 | obj/ 25 | Obj/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | /wwwroot/dist/ 30 | 31 | # MSTest test Results 32 | [Tt]est[Rr]esult*/ 33 | [Bb]uild[Ll]og.* 34 | 35 | # NUNIT 36 | *.VisualState.xml 37 | TestResult.xml 38 | 39 | # Build Results of an ATL Project 40 | [Dd]ebugPS/ 41 | [Rr]eleasePS/ 42 | dlldata.c 43 | 44 | *_i.c 45 | *_p.c 46 | *_i.h 47 | *.ilk 48 | *.meta 49 | *.obj 50 | *.pch 51 | *.pdb 52 | *.pgc 53 | *.pgd 54 | *.rsp 55 | *.sbr 56 | *.tlb 57 | *.tli 58 | *.tlh 59 | *.tmp 60 | *.tmp_proj 61 | *.log 62 | *.vspscc 63 | *.vssscc 64 | .builds 65 | *.pidb 66 | *.svclog 67 | *.scc 68 | 69 | # Chutzpah Test files 70 | _Chutzpah* 71 | 72 | # Visual C++ cache files 73 | ipch/ 74 | *.aps 75 | *.ncb 76 | *.opendb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | *.sap 86 | 87 | # TFS 2012 Local Workspace 88 | $tf/ 89 | 90 | # Guidance Automation Toolkit 91 | *.gpState 92 | 93 | # ReSharper is a .NET coding add-in 94 | _ReSharper*/ 95 | *.[Rr]e[Ss]harper 96 | *.DotSettings.user 97 | 98 | # JustCode is a .NET coding add-in 99 | .JustCode 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | _NCrunch_* 109 | .*crunch*.local.xml 110 | nCrunchTemp_* 111 | 112 | # MightyMoose 113 | *.mm.* 114 | AutoTest.Net/ 115 | 116 | # Web workbench (sass) 117 | .sass-cache/ 118 | 119 | # Installshield output folder 120 | [Ee]xpress/ 121 | 122 | # DocProject is a documentation generator add-in 123 | DocProject/buildhelp/ 124 | DocProject/Help/*.HxT 125 | DocProject/Help/*.HxC 126 | DocProject/Help/*.hhc 127 | DocProject/Help/*.hhk 128 | DocProject/Help/*.hhp 129 | DocProject/Help/Html2 130 | DocProject/Help/html 131 | 132 | # Click-Once directory 133 | publish/ 134 | 135 | # Publish Web Output 136 | *.[Pp]ublish.xml 137 | *.azurePubxml 138 | # TODO: Comment the next line if you want to checkin your web deploy settings 139 | # but database connection strings (with potential passwords) will be unencrypted 140 | *.pubxml 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Microsoft Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Microsoft Azure Emulator 157 | ecf/ 158 | rcf/ 159 | 160 | # Microsoft Azure ApplicationInsights config file 161 | ApplicationInsights.config 162 | 163 | # Windows Store app package directory 164 | AppPackages/ 165 | BundleArtifacts/ 166 | 167 | # Visual Studio cache files 168 | # files ending in .cache can be ignored 169 | *.[Cc]ache 170 | # but keep track of directories ending in .cache 171 | !*.[Cc]ache/ 172 | 173 | # Others 174 | ClientBin/ 175 | ~$* 176 | *~ 177 | *.dbmdl 178 | *.dbproj.schemaview 179 | *.pfx 180 | *.publishsettings 181 | orleans.codegen.cs 182 | 183 | /node_modules 184 | 185 | # RIA/Silverlight projects 186 | Generated_Code/ 187 | 188 | # Backup & report files from converting an old project file 189 | # to a newer Visual Studio version. Backup files are not needed, 190 | # because we have git ;-) 191 | _UpgradeReport_Files/ 192 | Backup*/ 193 | UpgradeLog*.XML 194 | UpgradeLog*.htm 195 | 196 | # SQL Server files 197 | *.mdf 198 | *.ldf 199 | 200 | # Business Intelligence projects 201 | *.rdl.data 202 | *.bim.layout 203 | *.bim_*.settings 204 | 205 | # Microsoft Fakes 206 | FakesAssemblies/ 207 | 208 | # GhostDoc plugin setting file 209 | *.GhostDoc.xml 210 | 211 | # Node.js Tools for Visual Studio 212 | .ntvs_analysis.dat 213 | 214 | # Visual Studio 6 build log 215 | *.plg 216 | 217 | # Visual Studio 6 workspace options file 218 | *.opt 219 | 220 | # Visual Studio LightSwitch build output 221 | **/*.HTMLClient/GeneratedArtifacts 222 | **/*.DesktopClient/GeneratedArtifacts 223 | **/*.DesktopClient/ModelManifest.xml 224 | **/*.Server/GeneratedArtifacts 225 | **/*.Server/ModelManifest.xml 226 | _Pvt_Extensions 227 | 228 | # Paket dependency manager 229 | .paket/paket.exe 230 | 231 | # FAKE - F# Make 232 | .fake/ 233 | -------------------------------------------------------------------------------- /.idea/.idea.ASPNETReact/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /projectSettingsUpdater.xml 6 | /contentModel.xml 7 | /modules.xml 8 | /.idea.ASPNETReact.iml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | # Datasource local storage ignored files 12 | /dataSources/ 13 | /dataSources.local.xml 14 | -------------------------------------------------------------------------------- /.idea/.idea.ASPNETReact/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.ASPNETReact/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.ASPNETReact/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ASPNETReact.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | true 7 | Latest 8 | false 9 | ClientApp\ 10 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 11 | https://localhost:44498 12 | npm start 13 | enable 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | wwwroot\%(RecursiveDir)%(FileName)%(Extension) 54 | PreserveNewest 55 | true 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ASPNETReact.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32407.343 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ASPNETReact", "ASPNETReact.csproj", "{71AC8596-78AF-4C26-8B1C-F0B834214592}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {71AC8596-78AF-4C26-8B1C-F0B834214592}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {71AC8596-78AF-4C26-8B1C-F0B834214592}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {71AC8596-78AF-4C26-8B1C-F0B834214592}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {71AC8596-78AF-4C26-8B1C-F0B834214592}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9A0C419F-568D-487E-9ADB-FB84AF97D312} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /ClientApp/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | -------------------------------------------------------------------------------- /ClientApp/.env.development: -------------------------------------------------------------------------------- 1 | PORT=44498 2 | HTTPS=true 3 | 4 | -------------------------------------------------------------------------------- /ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /ClientApp/aspnetcore-https.js: -------------------------------------------------------------------------------- 1 | // This script sets up HTTPS for the application using the ASP.NET Core HTTPS certificate 2 | const fs = require('fs'); 3 | const spawn = require('child_process').spawn; 4 | const path = require('path'); 5 | 6 | const baseFolder = 7 | process.env.APPDATA !== undefined && process.env.APPDATA !== '' 8 | ? `${process.env.APPDATA}/ASP.NET/https` 9 | : `${process.env.HOME}/.aspnet/https`; 10 | 11 | const certificateArg = process.argv.map(arg => arg.match(/--name=(?.+)/i)).filter(Boolean)[0]; 12 | const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name; 13 | 14 | if (!certificateName) { 15 | console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly.') 16 | process.exit(-1); 17 | } 18 | 19 | const certFilePath = path.join(baseFolder, `${certificateName}.pem`); 20 | const keyFilePath = path.join(baseFolder, `${certificateName}.key`); 21 | 22 | if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { 23 | spawn('dotnet', [ 24 | 'dev-certs', 25 | 'https', 26 | '--export-path', 27 | certFilePath, 28 | '--format', 29 | 'Pem', 30 | '--no-password', 31 | ], { stdio: 'inherit', }) 32 | .on('exit', (code) => process.exit(code)); 33 | } 34 | -------------------------------------------------------------------------------- /ClientApp/aspnetcore-react.js: -------------------------------------------------------------------------------- 1 | // This script configures the .env.development.local file with additional environment variables to configure HTTPS using the ASP.NET Core 2 | // development certificate in the webpack development proxy. 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const baseFolder = 8 | process.env.APPDATA !== undefined && process.env.APPDATA !== '' 9 | ? `${process.env.APPDATA}/ASP.NET/https` 10 | : `${process.env.HOME}/.aspnet/https`; 11 | 12 | const certificateArg = process.argv.map(arg => arg.match(/--name=(?.+)/i)).filter(Boolean)[0]; 13 | const certificateName = certificateArg ? certificateArg.groups.value : process.env.npm_package_name; 14 | 15 | if (!certificateName) { 16 | console.error('Invalid certificate name. Run this script in the context of an npm/yarn script or pass --name=<> explicitly.') 17 | process.exit(-1); 18 | } 19 | 20 | const certFilePath = path.join(baseFolder, `${certificateName}.pem`); 21 | const keyFilePath = path.join(baseFolder, `${certificateName}.key`); 22 | 23 | if (!fs.existsSync('.env.development.local')) { 24 | fs.writeFileSync( 25 | '.env.development.local', 26 | `SSL_CRT_FILE=${certFilePath} 27 | SSL_KEY_FILE=${keyFilePath}` 28 | ); 29 | } else { 30 | let lines = fs.readFileSync('.env.development.local') 31 | .toString() 32 | .split('\n'); 33 | 34 | let hasCert, hasCertKey = false; 35 | for (const line of lines) { 36 | if (/SSL_CRT_FILE=.*/i.test(line)) { 37 | hasCert = true; 38 | } 39 | if (/SSL_KEY_FILE=.*/i.test(line)) { 40 | hasCertKey = true; 41 | } 42 | } 43 | if (!hasCert) { 44 | fs.appendFileSync( 45 | '.env.development.local', 46 | `\nSSL_CRT_FILE=${certFilePath}` 47 | ); 48 | } 49 | if (!hasCertKey) { 50 | fs.appendFileSync( 51 | '.env.development.local', 52 | `\nSSL_KEY_FILE=${keyFilePath}` 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aspnetreact", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "bootstrap": "^5.1.3", 7 | "http-proxy-middleware": "^0.19.1", 8 | "jquery": "^3.6.0", 9 | "merge": "^2.1.1", 10 | "oidc-client": "^1.11.5", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-router-bootstrap": "^0.25.0", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "^0.9.5", 16 | "reactstrap": "^8.9.0", 17 | "rimraf": "^2.6.2", 18 | "web-vitals": "^0.2.4", 19 | "workbox-background-sync": "^5.1.3", 20 | "workbox-broadcast-update": "^5.1.3", 21 | "workbox-cacheable-response": "^5.1.3", 22 | "workbox-core": "^5.1.3", 23 | "workbox-expiration": "^5.1.3", 24 | "workbox-google-analytics": "^5.1.3", 25 | "workbox-navigation-preload": "^5.1.3", 26 | "workbox-precaching": "^5.1.3", 27 | "workbox-range-requests": "^5.1.3", 28 | "workbox-routing": "^5.1.3", 29 | "workbox-strategies": "^5.1.3", 30 | "workbox-streams": "^5.1.3" 31 | }, 32 | "devDependencies": { 33 | "ajv": "^6.9.1", 34 | "cross-env": "^7.0.3", 35 | "eslint": "^7.25.0", 36 | "eslint-config-react-app": "^6.0.0", 37 | "eslint-plugin-flowtype": "^5.7.2", 38 | "eslint-plugin-import": "^2.22.1", 39 | "eslint-plugin-jsx-a11y": "^6.4.1", 40 | "eslint-plugin-react": "^7.23.2", 41 | "nan": "^2.14.2", 42 | "typescript": "^4.2.4" 43 | }, 44 | "scripts": { 45 | "prestart": "node aspnetcore-https && node aspnetcore-react", 46 | "start": "rimraf ./build && react-scripts start", 47 | "build": "react-scripts build", 48 | "test": "cross-env CI=true react-scripts test --env=jsdom", 49 | "eject": "react-scripts eject", 50 | "lint": "eslint ./src/" 51 | }, 52 | "eslintConfig": { 53 | "extends": [ 54 | "react-app" 55 | ] 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ClientApp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirzaf/ASPNETReact/037177120985165041a3234694044e6e3efe5415/ClientApp/public/favicon.ico -------------------------------------------------------------------------------- /ClientApp/public/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | ASPNETReact 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ClientApp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ASPNETReact", 3 | "name": "ASPNETReact", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ClientApp/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Route } from 'react-router'; 3 | import { Layout } from './components/Layout'; 4 | import { Home } from './components/Home'; 5 | import { FetchData } from './components/FetchData'; 6 | import { Counter } from './components/Counter'; 7 | import AuthorizeRoute from './components/api-authorization/AuthorizeRoute'; 8 | import ApiAuthorizationRoutes from './components/api-authorization/ApiAuthorizationRoutes'; 9 | import { ApplicationPaths } from './components/api-authorization/ApiAuthorizationConstants'; 10 | 11 | import './custom.css' 12 | 13 | export default class App extends Component { 14 | static displayName = App.name; 15 | 16 | render () { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ClientApp/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | 6 | it('renders without crashing', async () => { 7 | const div = document.createElement('div'); 8 | ReactDOM.render( 9 | 10 | 11 | , div); 12 | await new Promise(resolve => setTimeout(resolve, 1000)); 13 | }); 14 | -------------------------------------------------------------------------------- /ClientApp/src/components/Counter.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class Counter extends Component { 4 | static displayName = Counter.name; 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = { currentCount: 0 }; 9 | this.incrementCounter = this.incrementCounter.bind(this); 10 | } 11 | 12 | incrementCounter() { 13 | this.setState({ 14 | currentCount: this.state.currentCount + 1 15 | }); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 |

Counter

22 | 23 |

This is a simple example of a React component.

24 | 25 |

Current count: {this.state.currentCount}

26 | 27 | 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ClientApp/src/components/FetchData.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import authService from './api-authorization/AuthorizeService' 3 | 4 | export class FetchData extends Component { 5 | static displayName = FetchData.name; 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { forecasts: [], loading: true }; 10 | } 11 | 12 | componentDidMount() { 13 | this.populateWeatherData(); 14 | } 15 | 16 | static renderForecastsTable(forecasts) { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {forecasts.map(forecast => 29 | 30 | 31 | 32 | 33 | 34 | 35 | )} 36 | 37 |
DateTemp. (C)Temp. (F)Summary
{forecast.date}{forecast.temperatureC}{forecast.temperatureF}{forecast.summary}
38 | ); 39 | } 40 | 41 | render() { 42 | let contents = this.state.loading 43 | ?

Loading...

44 | : FetchData.renderForecastsTable(this.state.forecasts); 45 | 46 | return ( 47 |
48 |

Weather forecast

49 |

This component demonstrates fetching data from the server.

50 | {contents} 51 |
52 | ); 53 | } 54 | 55 | async populateWeatherData() { 56 | const token = await authService.getAccessToken(); 57 | const response = await fetch('weatherforecast', { 58 | headers: !token ? {} : { 'Authorization': `Bearer ${token}` } 59 | }); 60 | const data = await response.json(); 61 | this.setState({ forecasts: data, loading: false }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ClientApp/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export class Home extends Component { 4 | static displayName = Home.name; 5 | 6 | render () { 7 | return ( 8 |
9 |

Hello, world!

10 |

Welcome to your new single-page application, built with:

11 | 16 |

To help you get started, we have also set up:

17 |
    18 |
  • Client-side navigation. For example, click Counter then Back to return here.
  • 19 |
  • Development server integration. In development mode, the development server from create-react-app runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.
  • 20 |
  • Efficient production builds. In production mode, development-time features are disabled, and your dotnet publish configuration produces minified, efficiently bundled JavaScript files.
  • 21 |
22 |

The ClientApp subdirectory is a standard React application based on the create-react-app template. If you open a command prompt in that directory, you can run npm commands such as npm test or npm install.

23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ClientApp/src/components/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Container } from 'reactstrap'; 3 | import { NavMenu } from './NavMenu'; 4 | 5 | export class Layout extends Component { 6 | static displayName = Layout.name; 7 | 8 | render () { 9 | return ( 10 |
11 | 12 | 13 | {this.props.children} 14 | 15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ClientApp/src/components/NavMenu.css: -------------------------------------------------------------------------------- 1 | a.navbar-brand { 2 | white-space: normal; 3 | text-align: center; 4 | word-break: break-all; 5 | } 6 | 7 | html { 8 | font-size: 14px; 9 | } 10 | @media (min-width: 768px) { 11 | html { 12 | font-size: 16px; 13 | } 14 | } 15 | 16 | .box-shadow { 17 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 18 | } 19 | -------------------------------------------------------------------------------- /ClientApp/src/components/NavMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Collapse, Container, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import { LoginMenu } from './api-authorization/LoginMenu'; 5 | import './NavMenu.css'; 6 | 7 | export class NavMenu extends Component { 8 | static displayName = NavMenu.name; 9 | 10 | constructor (props) { 11 | super(props); 12 | 13 | this.toggleNavbar = this.toggleNavbar.bind(this); 14 | this.state = { 15 | collapsed: true 16 | }; 17 | } 18 | 19 | toggleNavbar () { 20 | this.setState({ 21 | collapsed: !this.state.collapsed 22 | }); 23 | } 24 | 25 | render () { 26 | return ( 27 |
28 | 29 | 30 | ASPNETReact 31 | 32 | 33 |
    34 | 35 | Home 36 | 37 | 38 | Counter 39 | 40 | 41 | Fetch data 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ClientApp/src/components/api-authorization/ApiAuthorizationConstants.js: -------------------------------------------------------------------------------- 1 | export const ApplicationName = 'ASPNETReact'; 2 | 3 | export const QueryParameterNames = { 4 | ReturnUrl: 'returnUrl', 5 | Message: 'message' 6 | }; 7 | 8 | export const LogoutActions = { 9 | LogoutCallback: 'logout-callback', 10 | Logout: 'logout', 11 | LoggedOut: 'logged-out' 12 | }; 13 | 14 | export const LoginActions = { 15 | Login: 'login', 16 | LoginCallback: 'login-callback', 17 | LoginFailed: 'login-failed', 18 | Profile: 'profile', 19 | Register: 'register' 20 | }; 21 | 22 | const prefix = '/authentication'; 23 | 24 | export const ApplicationPaths = { 25 | DefaultLoginRedirectPath: '/', 26 | ApiAuthorizationClientConfigurationUrl: `_configuration/${ApplicationName}`, 27 | ApiAuthorizationPrefix: prefix, 28 | Login: `${prefix}/${LoginActions.Login}`, 29 | LoginFailed: `${prefix}/${LoginActions.LoginFailed}`, 30 | LoginCallback: `${prefix}/${LoginActions.LoginCallback}`, 31 | Register: `${prefix}/${LoginActions.Register}`, 32 | Profile: `${prefix}/${LoginActions.Profile}`, 33 | LogOut: `${prefix}/${LogoutActions.Logout}`, 34 | LoggedOut: `${prefix}/${LogoutActions.LoggedOut}`, 35 | LogOutCallback: `${prefix}/${LogoutActions.LogoutCallback}`, 36 | IdentityRegisterPath: 'Identity/Account/Register', 37 | IdentityManagePath: 'Identity/Account/Manage' 38 | }; 39 | -------------------------------------------------------------------------------- /ClientApp/src/components/api-authorization/ApiAuthorizationRoutes.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Route } from 'react-router'; 3 | import { Login } from './Login' 4 | import { Logout } from './Logout' 5 | import { ApplicationPaths, LoginActions, LogoutActions } from './ApiAuthorizationConstants'; 6 | 7 | export default class ApiAuthorizationRoutes extends Component { 8 | 9 | render () { 10 | return( 11 | 12 | loginAction(LoginActions.Login)} /> 13 | loginAction(LoginActions.LoginFailed)} /> 14 | loginAction(LoginActions.LoginCallback)} /> 15 | loginAction(LoginActions.Profile)} /> 16 | loginAction(LoginActions.Register)} /> 17 | logoutAction(LogoutActions.Logout)} /> 18 | logoutAction(LogoutActions.LogoutCallback)} /> 19 | logoutAction(LogoutActions.LoggedOut)} /> 20 | ); 21 | } 22 | } 23 | 24 | function loginAction(name){ 25 | return (); 26 | } 27 | 28 | function logoutAction(name) { 29 | return (); 30 | } 31 | -------------------------------------------------------------------------------- /ClientApp/src/components/api-authorization/AuthorizeRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react' 3 | import { Route, Redirect } from 'react-router-dom' 4 | import { ApplicationPaths, QueryParameterNames } from './ApiAuthorizationConstants' 5 | import authService from './AuthorizeService' 6 | 7 | export default class AuthorizeRoute extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | ready: false, 13 | authenticated: false 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | this._subscription = authService.subscribe(() => this.authenticationChanged()); 19 | this.populateAuthenticationState(); 20 | } 21 | 22 | componentWillUnmount() { 23 | authService.unsubscribe(this._subscription); 24 | } 25 | 26 | render() { 27 | const { ready, authenticated } = this.state; 28 | var link = document.createElement("a"); 29 | link.href = this.props.path; 30 | const returnUrl = `${link.protocol}//${link.host}${link.pathname}${link.search}${link.hash}`; 31 | const redirectUrl = `${ApplicationPaths.Login}?${QueryParameterNames.ReturnUrl}=${encodeURIComponent(returnUrl)}` 32 | if (!ready) { 33 | return
; 34 | } else { 35 | const { component: Component, ...rest } = this.props; 36 | return { 38 | if (authenticated) { 39 | return 40 | } else { 41 | return 42 | } 43 | }} /> 44 | } 45 | } 46 | 47 | async populateAuthenticationState() { 48 | const authenticated = await authService.isAuthenticated(); 49 | this.setState({ ready: true, authenticated }); 50 | } 51 | 52 | async authenticationChanged() { 53 | this.setState({ ready: false, authenticated: false }); 54 | await this.populateAuthenticationState(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ClientApp/src/components/api-authorization/AuthorizeService.js: -------------------------------------------------------------------------------- 1 | import { UserManager, WebStorageStateStore } from 'oidc-client'; 2 | import { ApplicationPaths, ApplicationName } from './ApiAuthorizationConstants'; 3 | 4 | export class AuthorizeService { 5 | _callbacks = []; 6 | _nextSubscriptionId = 0; 7 | _user = null; 8 | _isAuthenticated = false; 9 | 10 | // By default pop ups are disabled because they don't work properly on Edge. 11 | // If you want to enable pop up authentication simply set this flag to false. 12 | _popUpDisabled = true; 13 | 14 | async isAuthenticated() { 15 | const user = await this.getUser(); 16 | return !!user; 17 | } 18 | 19 | async getUser() { 20 | if (this._user && this._user.profile) { 21 | return this._user.profile; 22 | } 23 | 24 | await this.ensureUserManagerInitialized(); 25 | const user = await this.userManager.getUser(); 26 | return user && user.profile; 27 | } 28 | 29 | async getAccessToken() { 30 | await this.ensureUserManagerInitialized(); 31 | const user = await this.userManager.getUser(); 32 | return user && user.access_token; 33 | } 34 | 35 | // We try to authenticate the user in three different ways: 36 | // 1) We try to see if we can authenticate the user silently. This happens 37 | // when the user is already logged in on the IdP and is done using a hidden iframe 38 | // on the client. 39 | // 2) We try to authenticate the user using a PopUp Window. This might fail if there is a 40 | // Pop-Up blocker or the user has disabled PopUps. 41 | // 3) If the two methods above fail, we redirect the browser to the IdP to perform a traditional 42 | // redirect flow. 43 | async signIn(state) { 44 | await this.ensureUserManagerInitialized(); 45 | try { 46 | const silentUser = await this.userManager.signinSilent(this.createArguments()); 47 | this.updateState(silentUser); 48 | return this.success(state); 49 | } catch (silentError) { 50 | // User might not be authenticated, fallback to popup authentication 51 | console.log("Silent authentication error: ", silentError); 52 | 53 | try { 54 | if (this._popUpDisabled) { 55 | throw new Error('Popup disabled. Change \'AuthorizeService.js:AuthorizeService._popupDisabled\' to false to enable it.') 56 | } 57 | 58 | const popUpUser = await this.userManager.signinPopup(this.createArguments()); 59 | this.updateState(popUpUser); 60 | return this.success(state); 61 | } catch (popUpError) { 62 | if (popUpError.message === "Popup window closed") { 63 | // The user explicitly cancelled the login action by closing an opened popup. 64 | return this.error("The user closed the window."); 65 | } else if (!this._popUpDisabled) { 66 | console.log("Popup authentication error: ", popUpError); 67 | } 68 | 69 | // PopUps might be blocked by the user, fallback to redirect 70 | try { 71 | await this.userManager.signinRedirect(this.createArguments(state)); 72 | return this.redirect(); 73 | } catch (redirectError) { 74 | console.log("Redirect authentication error: ", redirectError); 75 | return this.error(redirectError); 76 | } 77 | } 78 | } 79 | } 80 | 81 | async completeSignIn(url) { 82 | try { 83 | await this.ensureUserManagerInitialized(); 84 | const user = await this.userManager.signinCallback(url); 85 | this.updateState(user); 86 | return this.success(user && user.state); 87 | } catch (error) { 88 | console.log('There was an error signing in: ', error); 89 | return this.error('There was an error signing in.'); 90 | } 91 | } 92 | 93 | // We try to sign out the user in two different ways: 94 | // 1) We try to do a sign-out using a PopUp Window. This might fail if there is a 95 | // Pop-Up blocker or the user has disabled PopUps. 96 | // 2) If the method above fails, we redirect the browser to the IdP to perform a traditional 97 | // post logout redirect flow. 98 | async signOut(state) { 99 | await this.ensureUserManagerInitialized(); 100 | try { 101 | if (this._popUpDisabled) { 102 | throw new Error('Popup disabled. Change \'AuthorizeService.js:AuthorizeService._popupDisabled\' to false to enable it.') 103 | } 104 | 105 | await this.userManager.signoutPopup(this.createArguments()); 106 | this.updateState(undefined); 107 | return this.success(state); 108 | } catch (popupSignOutError) { 109 | console.log("Popup signout error: ", popupSignOutError); 110 | try { 111 | await this.userManager.signoutRedirect(this.createArguments(state)); 112 | return this.redirect(); 113 | } catch (redirectSignOutError) { 114 | console.log("Redirect signout error: ", redirectSignOutError); 115 | return this.error(redirectSignOutError); 116 | } 117 | } 118 | } 119 | 120 | async completeSignOut(url) { 121 | await this.ensureUserManagerInitialized(); 122 | try { 123 | const response = await this.userManager.signoutCallback(url); 124 | this.updateState(null); 125 | return this.success(response && response.data); 126 | } catch (error) { 127 | console.log(`There was an error trying to log out '${error}'.`); 128 | return this.error(error); 129 | } 130 | } 131 | 132 | updateState(user) { 133 | this._user = user; 134 | this._isAuthenticated = !!this._user; 135 | this.notifySubscribers(); 136 | } 137 | 138 | subscribe(callback) { 139 | this._callbacks.push({ callback, subscription: this._nextSubscriptionId++ }); 140 | return this._nextSubscriptionId - 1; 141 | } 142 | 143 | unsubscribe(subscriptionId) { 144 | const subscriptionIndex = this._callbacks 145 | .map((element, index) => element.subscription === subscriptionId ? { found: true, index } : { found: false }) 146 | .filter(element => element.found === true); 147 | if (subscriptionIndex.length !== 1) { 148 | throw new Error(`Found an invalid number of subscriptions ${subscriptionIndex.length}`); 149 | } 150 | 151 | this._callbacks.splice(subscriptionIndex[0].index, 1); 152 | } 153 | 154 | notifySubscribers() { 155 | for (let i = 0; i < this._callbacks.length; i++) { 156 | const callback = this._callbacks[i].callback; 157 | callback(); 158 | } 159 | } 160 | 161 | createArguments(state) { 162 | return { useReplaceToNavigate: true, data: state }; 163 | } 164 | 165 | error(message) { 166 | return { status: AuthenticationResultStatus.Fail, message }; 167 | } 168 | 169 | success(state) { 170 | return { status: AuthenticationResultStatus.Success, state }; 171 | } 172 | 173 | redirect() { 174 | return { status: AuthenticationResultStatus.Redirect }; 175 | } 176 | 177 | async ensureUserManagerInitialized() { 178 | if (this.userManager !== undefined) { 179 | return; 180 | } 181 | 182 | let response = await fetch(ApplicationPaths.ApiAuthorizationClientConfigurationUrl); 183 | if (!response.ok) { 184 | throw new Error(`Could not load settings for '${ApplicationName}'`); 185 | } 186 | 187 | let settings = await response.json(); 188 | settings.automaticSilentRenew = true; 189 | settings.includeIdTokenInSilentRenew = true; 190 | settings.userStore = new WebStorageStateStore({ 191 | prefix: ApplicationName 192 | }); 193 | 194 | this.userManager = new UserManager(settings); 195 | 196 | this.userManager.events.addUserSignedOut(async () => { 197 | await this.userManager.removeUser(); 198 | this.updateState(undefined); 199 | }); 200 | } 201 | 202 | static get instance() { return authService } 203 | } 204 | 205 | const authService = new AuthorizeService(); 206 | 207 | export default authService; 208 | 209 | export const AuthenticationResultStatus = { 210 | Redirect: 'redirect', 211 | Success: 'success', 212 | Fail: 'fail' 213 | }; 214 | -------------------------------------------------------------------------------- /ClientApp/src/components/api-authorization/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react'; 3 | import authService from './AuthorizeService'; 4 | import { AuthenticationResultStatus } from './AuthorizeService'; 5 | import { LoginActions, QueryParameterNames, ApplicationPaths } from './ApiAuthorizationConstants'; 6 | 7 | // The main responsibility of this component is to handle the user's login process. 8 | // This is the starting point for the login process. Any component that needs to authenticate 9 | // a user can simply perform a redirect to this component with a returnUrl query parameter and 10 | // let the component perform the login and return back to the return url. 11 | export class Login extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | message: undefined 17 | }; 18 | } 19 | 20 | componentDidMount() { 21 | const action = this.props.action; 22 | switch (action) { 23 | case LoginActions.Login: 24 | this.login(this.getReturnUrl()); 25 | break; 26 | case LoginActions.LoginCallback: 27 | this.processLoginCallback(); 28 | break; 29 | case LoginActions.LoginFailed: 30 | const params = new URLSearchParams(window.location.search); 31 | const error = params.get(QueryParameterNames.Message); 32 | this.setState({ message: error }); 33 | break; 34 | case LoginActions.Profile: 35 | this.redirectToProfile(); 36 | break; 37 | case LoginActions.Register: 38 | this.redirectToRegister(); 39 | break; 40 | default: 41 | throw new Error(`Invalid action '${action}'`); 42 | } 43 | } 44 | 45 | render() { 46 | const action = this.props.action; 47 | const { message } = this.state; 48 | 49 | if (!!message) { 50 | return
{message}
51 | } else { 52 | switch (action) { 53 | case LoginActions.Login: 54 | return (
Processing login
); 55 | case LoginActions.LoginCallback: 56 | return (
Processing login callback
); 57 | case LoginActions.Profile: 58 | case LoginActions.Register: 59 | return (
); 60 | default: 61 | throw new Error(`Invalid action '${action}'`); 62 | } 63 | } 64 | } 65 | 66 | async login(returnUrl) { 67 | const state = { returnUrl }; 68 | const result = await authService.signIn(state); 69 | switch (result.status) { 70 | case AuthenticationResultStatus.Redirect: 71 | break; 72 | case AuthenticationResultStatus.Success: 73 | await this.navigateToReturnUrl(returnUrl); 74 | break; 75 | case AuthenticationResultStatus.Fail: 76 | this.setState({ message: result.message }); 77 | break; 78 | default: 79 | throw new Error(`Invalid status result ${result.status}.`); 80 | } 81 | } 82 | 83 | async processLoginCallback() { 84 | const url = window.location.href; 85 | const result = await authService.completeSignIn(url); 86 | switch (result.status) { 87 | case AuthenticationResultStatus.Redirect: 88 | // There should not be any redirects as the only time completeSignIn finishes 89 | // is when we are doing a redirect sign in flow. 90 | throw new Error('Should not redirect.'); 91 | case AuthenticationResultStatus.Success: 92 | await this.navigateToReturnUrl(this.getReturnUrl(result.state)); 93 | break; 94 | case AuthenticationResultStatus.Fail: 95 | this.setState({ message: result.message }); 96 | break; 97 | default: 98 | throw new Error(`Invalid authentication result status '${result.status}'.`); 99 | } 100 | } 101 | 102 | getReturnUrl(state) { 103 | const params = new URLSearchParams(window.location.search); 104 | const fromQuery = params.get(QueryParameterNames.ReturnUrl); 105 | if (fromQuery && !fromQuery.startsWith(`${window.location.origin}/`)) { 106 | // This is an extra check to prevent open redirects. 107 | throw new Error("Invalid return url. The return url needs to have the same origin as the current page.") 108 | } 109 | return (state && state.returnUrl) || fromQuery || `${window.location.origin}/`; 110 | } 111 | 112 | redirectToRegister() { 113 | this.redirectToApiAuthorizationPath(`${ApplicationPaths.IdentityRegisterPath}?${QueryParameterNames.ReturnUrl}=${encodeURI(ApplicationPaths.Login)}`); 114 | } 115 | 116 | redirectToProfile() { 117 | this.redirectToApiAuthorizationPath(ApplicationPaths.IdentityManagePath); 118 | } 119 | 120 | redirectToApiAuthorizationPath(apiAuthorizationPath) { 121 | const redirectUrl = `${window.location.origin}/${apiAuthorizationPath}`; 122 | // It's important that we do a replace here so that when the user hits the back arrow on the 123 | // browser they get sent back to where it was on the app instead of to an endpoint on this 124 | // component. 125 | window.location.replace(redirectUrl); 126 | } 127 | 128 | navigateToReturnUrl(returnUrl) { 129 | // It's important that we do a replace here so that we remove the callback uri with the 130 | // fragment containing the tokens from the browser history. 131 | window.location.replace(returnUrl); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /ClientApp/src/components/api-authorization/LoginMenu.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { NavItem, NavLink } from 'reactstrap'; 3 | import { Link } from 'react-router-dom'; 4 | import authService from './AuthorizeService'; 5 | import { ApplicationPaths } from './ApiAuthorizationConstants'; 6 | 7 | export class LoginMenu extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | isAuthenticated: false, 13 | userName: null 14 | }; 15 | } 16 | 17 | componentDidMount() { 18 | this._subscription = authService.subscribe(() => this.populateState()); 19 | this.populateState(); 20 | } 21 | 22 | componentWillUnmount() { 23 | authService.unsubscribe(this._subscription); 24 | } 25 | 26 | async populateState() { 27 | const [isAuthenticated, user] = await Promise.all([authService.isAuthenticated(), authService.getUser()]) 28 | this.setState({ 29 | isAuthenticated, 30 | userName: user && user.name 31 | }); 32 | } 33 | 34 | render() { 35 | const { isAuthenticated, userName } = this.state; 36 | if (!isAuthenticated) { 37 | const registerPath = `${ApplicationPaths.Register}`; 38 | const loginPath = `${ApplicationPaths.Login}`; 39 | return this.anonymousView(registerPath, loginPath); 40 | } else { 41 | const profilePath = `${ApplicationPaths.Profile}`; 42 | const logoutPath = { pathname: `${ApplicationPaths.LogOut}`, state: { local: true } }; 43 | return this.authenticatedView(userName, profilePath, logoutPath); 44 | } 45 | } 46 | 47 | authenticatedView(userName, profilePath, logoutPath) { 48 | return ( 49 | 50 | Hello {userName} 51 | 52 | 53 | Logout 54 | 55 | ); 56 | 57 | } 58 | 59 | anonymousView(registerPath, loginPath) { 60 | return ( 61 | 62 | Register 63 | 64 | 65 | Login 66 | 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ClientApp/src/components/api-authorization/Logout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Component } from 'react'; 3 | import authService from './AuthorizeService'; 4 | import { AuthenticationResultStatus } from './AuthorizeService'; 5 | import { QueryParameterNames, LogoutActions, ApplicationPaths } from './ApiAuthorizationConstants'; 6 | 7 | // The main responsibility of this component is to handle the user's logout process. 8 | // This is the starting point for the logout process, which is usually initiated when a 9 | // user clicks on the logout button on the LoginMenu component. 10 | export class Logout extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | message: undefined, 16 | isReady: false, 17 | authenticated: false 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | const action = this.props.action; 23 | switch (action) { 24 | case LogoutActions.Logout: 25 | if (!!window.history.state.state.local) { 26 | this.logout(this.getReturnUrl()); 27 | } else { 28 | // This prevents regular links to /authentication/logout from triggering a logout 29 | this.setState({ isReady: true, message: "The logout was not initiated from within the page." }); 30 | } 31 | break; 32 | case LogoutActions.LogoutCallback: 33 | this.processLogoutCallback(); 34 | break; 35 | case LogoutActions.LoggedOut: 36 | this.setState({ isReady: true, message: "You successfully logged out!" }); 37 | break; 38 | default: 39 | throw new Error(`Invalid action '${action}'`); 40 | } 41 | 42 | this.populateAuthenticationState(); 43 | } 44 | 45 | render() { 46 | const { isReady, message } = this.state; 47 | if (!isReady) { 48 | return
49 | } 50 | if (!!message) { 51 | return (
{message}
); 52 | } else { 53 | const action = this.props.action; 54 | switch (action) { 55 | case LogoutActions.Logout: 56 | return (
Processing logout
); 57 | case LogoutActions.LogoutCallback: 58 | return (
Processing logout callback
); 59 | case LogoutActions.LoggedOut: 60 | return (
{message}
); 61 | default: 62 | throw new Error(`Invalid action '${action}'`); 63 | } 64 | } 65 | } 66 | 67 | async logout(returnUrl) { 68 | const state = { returnUrl }; 69 | const isauthenticated = await authService.isAuthenticated(); 70 | if (isauthenticated) { 71 | const result = await authService.signOut(state); 72 | switch (result.status) { 73 | case AuthenticationResultStatus.Redirect: 74 | break; 75 | case AuthenticationResultStatus.Success: 76 | await this.navigateToReturnUrl(returnUrl); 77 | break; 78 | case AuthenticationResultStatus.Fail: 79 | this.setState({ message: result.message }); 80 | break; 81 | default: 82 | throw new Error("Invalid authentication result status."); 83 | } 84 | } else { 85 | this.setState({ message: "You successfully logged out!" }); 86 | } 87 | } 88 | 89 | async processLogoutCallback() { 90 | const url = window.location.href; 91 | const result = await authService.completeSignOut(url); 92 | switch (result.status) { 93 | case AuthenticationResultStatus.Redirect: 94 | // There should not be any redirects as the only time completeAuthentication finishes 95 | // is when we are doing a redirect sign in flow. 96 | throw new Error('Should not redirect.'); 97 | case AuthenticationResultStatus.Success: 98 | await this.navigateToReturnUrl(this.getReturnUrl(result.state)); 99 | break; 100 | case AuthenticationResultStatus.Fail: 101 | this.setState({ message: result.message }); 102 | break; 103 | default: 104 | throw new Error("Invalid authentication result status."); 105 | } 106 | } 107 | 108 | async populateAuthenticationState() { 109 | const authenticated = await authService.isAuthenticated(); 110 | this.setState({ isReady: true, authenticated }); 111 | } 112 | 113 | getReturnUrl(state) { 114 | const params = new URLSearchParams(window.location.search); 115 | const fromQuery = params.get(QueryParameterNames.ReturnUrl); 116 | if (fromQuery && !fromQuery.startsWith(`${window.location.origin}/`)) { 117 | // This is an extra check to prevent open redirects. 118 | throw new Error("Invalid return url. The return url needs to have the same origin as the current page.") 119 | } 120 | return (state && state.returnUrl) || 121 | fromQuery || 122 | `${window.location.origin}${ApplicationPaths.LoggedOut}`; 123 | } 124 | 125 | navigateToReturnUrl(returnUrl) { 126 | return window.location.replace(returnUrl); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /ClientApp/src/custom.css: -------------------------------------------------------------------------------- 1 | /* Provide sufficient contrast against white background */ 2 | a { 3 | color: #0366d6; 4 | } 5 | 6 | code { 7 | color: #E01A76; 8 | } 9 | 10 | .btn-primary { 11 | color: #fff; 12 | background-color: #1b6ec2; 13 | border-color: #1861ac; 14 | } 15 | -------------------------------------------------------------------------------- /ClientApp/src/index.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import App from './App'; 6 | import * as serviceWorkerRegistration from './serviceWorkerRegistration'; 7 | import reportWebVitals from './reportWebVitals'; 8 | 9 | const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href'); 10 | const rootElement = document.getElementById('root'); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | , 16 | rootElement); 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: https://cra.link/PWA 21 | serviceWorkerRegistration.unregister(); 22 | 23 | // If you want to start measuring performance in your app, pass a function 24 | // to log results (for example: reportWebVitals(console.log)) 25 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 26 | reportWebVitals(); 27 | -------------------------------------------------------------------------------- /ClientApp/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /ClientApp/src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | // This service worker can be customized! 4 | // See https://developers.google.com/web/tools/workbox/modules 5 | // for the list of available Workbox modules, or add any other 6 | // code you'd like. 7 | // You can also remove this file if you'd prefer not to use a 8 | // service worker, and the Workbox build step will be skipped. 9 | 10 | import { clientsClaim } from 'workbox-core'; 11 | import { ExpirationPlugin } from 'workbox-expiration'; 12 | import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; 13 | import { registerRoute } from 'workbox-routing'; 14 | import { StaleWhileRevalidate } from 'workbox-strategies'; 15 | 16 | clientsClaim(); 17 | 18 | // Precache all of the assets generated by your build process. 19 | // Their URLs are injected into the manifest variable below. 20 | // This variable must be present somewhere in your service worker file, 21 | // even if you decide not to use precaching. See https://cra.link/PWA 22 | precacheAndRoute(self.__WB_MANIFEST); 23 | 24 | // Set up App Shell-style routing, so that all navigation requests 25 | // are fulfilled with your index.html shell. Learn more at 26 | // https://developers.google.com/web/fundamentals/architecture/app-shell 27 | const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$'); 28 | registerRoute( 29 | // Return false to exempt requests from being fulfilled by index.html. 30 | ({ request, url }) => { 31 | // If this isn't a navigation, skip. 32 | if (request.mode !== 'navigate') { 33 | return false; 34 | } // If this is a URL that starts with /_, skip. 35 | 36 | if (url.pathname.startsWith('/_')) { 37 | return false; 38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip. 39 | 40 | if (url.pathname.match(fileExtensionRegexp)) { 41 | return false; 42 | } // Return true to signal that we want to use the handler. 43 | 44 | return true; 45 | }, 46 | createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') 47 | ); 48 | 49 | // An example runtime caching route for requests that aren't handled by the 50 | // precache, in this case same-origin .png requests like those from in public/ 51 | registerRoute( 52 | // Add in any other file extensions or routing criteria as needed. 53 | ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. 54 | new StaleWhileRevalidate({ 55 | cacheName: 'images', 56 | plugins: [ 57 | // Ensure that once this runtime cache reaches a maximum size the 58 | // least-recently used images are removed. 59 | new ExpirationPlugin({ maxEntries: 50 }), 60 | ], 61 | }) 62 | ); 63 | 64 | // This allows the web app to trigger skipWaiting via 65 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 66 | self.addEventListener('message', (event) => { 67 | if (event.data && event.data.type === 'SKIP_WAITING') { 68 | self.skipWaiting(); 69 | } 70 | }); 71 | 72 | // Any other custom service worker logic can go here. 73 | -------------------------------------------------------------------------------- /ClientApp/src/serviceWorkerRegistration.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://cra.link/PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) 19 | ); 20 | 21 | export function register(config) { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Let's check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl, config); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://cra.link/PWA' 45 | ); 46 | }); 47 | } else { 48 | // Is not localhost. Just register service worker 49 | registerValidSW(swUrl, config); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl, config) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then((registration) => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | if (installingWorker == null) { 62 | return; 63 | } 64 | installingWorker.onstatechange = () => { 65 | if (installingWorker.state === 'installed') { 66 | if (navigator.serviceWorker.controller) { 67 | // At this point, the updated precached content has been fetched, 68 | // but the previous service worker will still serve the older 69 | // content until all client tabs are closed. 70 | console.log( 71 | 'New content is available and will be used when all ' + 72 | 'tabs for this page are closed. See https://cra.link/PWA.' 73 | ); 74 | 75 | // Execute callback 76 | if (config && config.onUpdate) { 77 | config.onUpdate(registration); 78 | } 79 | } else { 80 | // At this point, everything has been precached. 81 | // It's the perfect time to display a 82 | // "Content is cached for offline use." message. 83 | console.log('Content is cached for offline use.'); 84 | 85 | // Execute callback 86 | if (config && config.onSuccess) { 87 | config.onSuccess(registration); 88 | } 89 | } 90 | } 91 | }; 92 | }; 93 | }) 94 | .catch((error) => { 95 | console.error('Error during service worker registration:', error); 96 | }); 97 | } 98 | 99 | function checkValidServiceWorker(swUrl, config) { 100 | // Check if the service worker can be found. If it can't reload the page. 101 | fetch(swUrl, { 102 | headers: { 'Service-Worker': 'script' }, 103 | }) 104 | .then((response) => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then((registration) => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log('No internet connection found. App is running in offline mode.'); 124 | }); 125 | } 126 | 127 | export function unregister() { 128 | if ('serviceWorker' in navigator) { 129 | navigator.serviceWorker.ready 130 | .then((registration) => { 131 | registration.unregister(); 132 | }) 133 | .catch((error) => { 134 | console.error(error.message); 135 | }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ClientApp/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | const createProxyMiddleware = require('http-proxy-middleware'); 2 | const { env } = require('process'); 3 | 4 | const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` : 5 | env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:65469'; 6 | 7 | const context = [ 8 | "/weatherforecast", 9 | "/_configuration", 10 | "/.well-known", 11 | "/Identity", 12 | "/connect", 13 | "/ApplyDatabaseMigrations", 14 | "/_framework" 15 | ]; 16 | 17 | module.exports = function(app) { 18 | const appProxy = createProxyMiddleware(context, { 19 | target: target, 20 | secure: false, 21 | headers: { 22 | Connection: 'Keep-Alive' 23 | } 24 | }); 25 | 26 | app.use(appProxy); 27 | }; 28 | -------------------------------------------------------------------------------- /ClientApp/src/setupTests.js: -------------------------------------------------------------------------------- 1 | const localStorageMock = { 2 | getItem: jest.fn(), 3 | setItem: jest.fn(), 4 | removeItem: jest.fn(), 5 | clear: jest.fn(), 6 | }; 7 | global.localStorage = localStorageMock; 8 | 9 | // Mock the request issued by the react app to get the client configuration parameters. 10 | window.fetch = () => { 11 | return Promise.resolve( 12 | { 13 | ok: true, 14 | json: () => Promise.resolve({ 15 | "authority": "https://localhost:7299", 16 | "client_id": "ASPNETReact", 17 | "redirect_uri": "https://localhost:7299/authentication/login-callback", 18 | "post_logout_redirect_uri": "https://localhost:7299/authentication/logout-callback", 19 | "response_type": "id_token token", 20 | "scope": "ASPNETReactAPI openid profile" 21 | }) 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /Controllers/OidcConfigurationController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace ASPNETReact.Controllers 5 | { 6 | public class OidcConfigurationController : Controller 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public OidcConfigurationController( 11 | IClientRequestParametersProvider clientRequestParametersProvider, 12 | ILogger logger) 13 | { 14 | ClientRequestParametersProvider = clientRequestParametersProvider; 15 | _logger = logger; 16 | } 17 | 18 | public IClientRequestParametersProvider ClientRequestParametersProvider { get; } 19 | 20 | [HttpGet("_configuration/{clientId}")] 21 | public IActionResult GetClientRequestParameters([FromRoute] string clientId) 22 | { 23 | var parameters = ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId); 24 | return Ok(parameters); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace ASPNETReact.Controllers 5 | { 6 | [Authorize] 7 | [ApiController] 8 | [Route("[controller]")] 9 | public class WeatherForecastController : ControllerBase 10 | { 11 | private static readonly string[] Summaries = new[] 12 | { 13 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 14 | }; 15 | 16 | private readonly ILogger _logger; 17 | 18 | public WeatherForecastController(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | [HttpGet] 24 | public IEnumerable Get() 25 | { 26 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 27 | { 28 | Date = DateTime.Now.AddDays(index), 29 | TemperatureC = Random.Shared.Next(-20, 55), 30 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 31 | }) 32 | .ToArray(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using ASPNETReact.Models; 2 | using Duende.IdentityServer.EntityFramework.Options; 3 | using Microsoft.AspNetCore.ApiAuthorization.IdentityServer; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace ASPNETReact.Data 8 | { 9 | public class ApplicationDbContext : ApiAuthorizationDbContext 10 | { 11 | public ApplicationDbContext(DbContextOptions options, IOptions operationalStoreOptions) 12 | : base(options, operationalStoreOptions) 13 | { 14 | 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using ASPNETReact.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using System; 9 | 10 | namespace ASPNETReact.Data.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("00000000000000_CreateIdentitySchema")] 14 | partial class CreateIdentitySchema 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .UseIdentityColumns() 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2"); 23 | 24 | modelBuilder.Entity("ASPNETReact.Models.ApplicationUser", b => 25 | { 26 | b.Property("Id") 27 | .HasColumnType("nvarchar(450)"); 28 | 29 | b.Property("AccessFailedCount") 30 | .HasColumnType("int"); 31 | 32 | b.Property("ConcurrencyStamp") 33 | .IsConcurrencyToken() 34 | .HasColumnType("nvarchar(max)"); 35 | 36 | b.Property("Email") 37 | .HasMaxLength(256) 38 | .HasColumnType("nvarchar(256)"); 39 | 40 | b.Property("EmailConfirmed") 41 | .HasColumnType("bit"); 42 | 43 | b.Property("LockoutEnabled") 44 | .HasColumnType("bit"); 45 | 46 | b.Property("LockoutEnd") 47 | .HasColumnType("datetimeoffset"); 48 | 49 | b.Property("NormalizedEmail") 50 | .HasMaxLength(256) 51 | .HasColumnType("nvarchar(256)"); 52 | 53 | b.Property("NormalizedUserName") 54 | .HasMaxLength(256) 55 | .HasColumnType("nvarchar(256)"); 56 | 57 | b.Property("PasswordHash") 58 | .HasColumnType("nvarchar(max)"); 59 | 60 | b.Property("PhoneNumber") 61 | .HasColumnType("nvarchar(max)"); 62 | 63 | b.Property("PhoneNumberConfirmed") 64 | .HasColumnType("bit"); 65 | 66 | b.Property("SecurityStamp") 67 | .HasColumnType("nvarchar(max)"); 68 | 69 | b.Property("TwoFactorEnabled") 70 | .HasColumnType("bit"); 71 | 72 | b.Property("UserName") 73 | .HasMaxLength(256) 74 | .HasColumnType("nvarchar(256)"); 75 | 76 | b.HasKey("Id"); 77 | 78 | b.HasIndex("NormalizedEmail") 79 | .HasDatabaseName("EmailIndex"); 80 | 81 | b.HasIndex("NormalizedUserName") 82 | .IsUnique() 83 | .HasDatabaseName("UserNameIndex") 84 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 85 | 86 | b.ToTable("AspNetUsers"); 87 | }); 88 | 89 | modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b => 90 | { 91 | b.Property("UserCode") 92 | .HasMaxLength(200) 93 | .HasColumnType("nvarchar(200)"); 94 | 95 | b.Property("ClientId") 96 | .IsRequired() 97 | .HasMaxLength(200) 98 | .HasColumnType("nvarchar(200)"); 99 | 100 | b.Property("CreationTime") 101 | .HasColumnType("datetime2"); 102 | 103 | b.Property("Data") 104 | .IsRequired() 105 | .HasMaxLength(52990) 106 | .HasColumnType("nvarchar(max)"); 107 | 108 | b.Property("Description") 109 | .HasMaxLength(200) 110 | .HasColumnType("nvarchar(200)"); 111 | 112 | b.Property("DeviceCode") 113 | .IsRequired() 114 | .HasMaxLength(200) 115 | .HasColumnType("nvarchar(200)"); 116 | 117 | b.Property("Expiration") 118 | .IsRequired() 119 | .HasColumnType("datetime2"); 120 | 121 | b.Property("SessionId") 122 | .HasMaxLength(100) 123 | .HasColumnType("nvarchar(100)"); 124 | 125 | b.Property("SubjectId") 126 | .HasMaxLength(200) 127 | .HasColumnType("nvarchar(200)"); 128 | 129 | b.HasKey("UserCode"); 130 | 131 | b.HasIndex("DeviceCode") 132 | .IsUnique(); 133 | 134 | b.HasIndex("Expiration"); 135 | 136 | b.ToTable("DeviceCodes"); 137 | }); 138 | 139 | modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => 140 | { 141 | b.Property("Id") 142 | .HasMaxLength(450) 143 | .HasColumnType("nvarchar(450)"); 144 | 145 | b.Property("Algorithm") 146 | .IsRequired() 147 | .HasMaxLength(100) 148 | .HasColumnType("nvarchar(100)"); 149 | 150 | b.Property("Created") 151 | .HasColumnType("datetime2"); 152 | 153 | b.Property("Data") 154 | .IsRequired() 155 | .HasColumnType("nvarchar(max)") 156 | .HasMaxLength(52990); 157 | 158 | b.Property("DataProtected") 159 | .HasColumnType("bit"); 160 | 161 | b.Property("IsX509Certificate") 162 | .HasColumnType("bit"); 163 | 164 | b.Property("Use") 165 | .HasMaxLength(450) 166 | .HasColumnType("nvarchar(450)"); 167 | 168 | b.Property("Version") 169 | .HasColumnType("int"); 170 | 171 | b.HasKey("Id"); 172 | 173 | b.HasIndex("Use"); 174 | 175 | b.ToTable("Keys"); 176 | }); 177 | 178 | modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => 179 | { 180 | b.Property("Key") 181 | .HasMaxLength(200) 182 | .HasColumnType("nvarchar(200)"); 183 | 184 | b.Property("ClientId") 185 | .IsRequired() 186 | .HasMaxLength(200) 187 | .HasColumnType("nvarchar(200)"); 188 | 189 | b.Property("ConsumedTime") 190 | .HasColumnType("datetime2"); 191 | 192 | b.Property("CreationTime") 193 | .HasColumnType("datetime2"); 194 | 195 | b.Property("Data") 196 | .IsRequired() 197 | .HasMaxLength(52990) 198 | .HasColumnType("nvarchar(max)"); 199 | 200 | b.Property("Description") 201 | .HasMaxLength(200) 202 | .HasColumnType("nvarchar(200)"); 203 | 204 | b.Property("Expiration") 205 | .HasColumnType("datetime2"); 206 | 207 | b.Property("SessionId") 208 | .HasMaxLength(100) 209 | .HasColumnType("nvarchar(100)"); 210 | 211 | b.Property("SubjectId") 212 | .HasMaxLength(200) 213 | .HasColumnType("nvarchar(200)"); 214 | 215 | b.Property("Type") 216 | .IsRequired() 217 | .HasMaxLength(50) 218 | .HasColumnType("nvarchar(50)"); 219 | 220 | b.HasKey("Key"); 221 | 222 | b.HasIndex("Expiration"); 223 | 224 | b.HasIndex("SubjectId", "ClientId", "Type"); 225 | 226 | b.HasIndex("SubjectId", "SessionId", "Type"); 227 | 228 | b.ToTable("PersistedGrants"); 229 | }); 230 | 231 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 232 | { 233 | b.Property("Id") 234 | .HasColumnType("nvarchar(450)"); 235 | 236 | b.Property("ConcurrencyStamp") 237 | .IsConcurrencyToken() 238 | .HasColumnType("nvarchar(max)"); 239 | 240 | b.Property("Name") 241 | .HasMaxLength(256) 242 | .HasColumnType("nvarchar(256)"); 243 | 244 | b.Property("NormalizedName") 245 | .HasMaxLength(256) 246 | .HasColumnType("nvarchar(256)"); 247 | 248 | b.HasKey("Id"); 249 | 250 | b.HasIndex("NormalizedName") 251 | .IsUnique() 252 | .HasDatabaseName("RoleNameIndex") 253 | .HasFilter("[NormalizedName] IS NOT NULL"); 254 | 255 | b.ToTable("AspNetRoles"); 256 | }); 257 | 258 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 259 | { 260 | b.Property("Id") 261 | .ValueGeneratedOnAdd() 262 | .HasColumnType("int") 263 | .UseIdentityColumn(); 264 | 265 | b.Property("ClaimType") 266 | .HasColumnType("nvarchar(max)"); 267 | 268 | b.Property("ClaimValue") 269 | .HasColumnType("nvarchar(max)"); 270 | 271 | b.Property("RoleId") 272 | .IsRequired() 273 | .HasColumnType("nvarchar(450)"); 274 | 275 | b.HasKey("Id"); 276 | 277 | b.HasIndex("RoleId"); 278 | 279 | b.ToTable("AspNetRoleClaims"); 280 | }); 281 | 282 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 283 | { 284 | b.Property("Id") 285 | .ValueGeneratedOnAdd() 286 | .HasColumnType("int") 287 | .UseIdentityColumn(); 288 | 289 | b.Property("ClaimType") 290 | .HasColumnType("nvarchar(max)"); 291 | 292 | b.Property("ClaimValue") 293 | .HasColumnType("nvarchar(max)"); 294 | 295 | b.Property("UserId") 296 | .IsRequired() 297 | .HasColumnType("nvarchar(450)"); 298 | 299 | b.HasKey("Id"); 300 | 301 | b.HasIndex("UserId"); 302 | 303 | b.ToTable("AspNetUserClaims"); 304 | }); 305 | 306 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 307 | { 308 | b.Property("LoginProvider") 309 | .HasMaxLength(128) 310 | .HasColumnType("nvarchar(128)"); 311 | 312 | b.Property("ProviderKey") 313 | .HasMaxLength(128) 314 | .HasColumnType("nvarchar(128)"); 315 | 316 | b.Property("ProviderDisplayName") 317 | .HasColumnType("nvarchar(max)"); 318 | 319 | b.Property("UserId") 320 | .IsRequired() 321 | .HasColumnType("nvarchar(450)"); 322 | 323 | b.HasKey("LoginProvider", "ProviderKey"); 324 | 325 | b.HasIndex("UserId"); 326 | 327 | b.ToTable("AspNetUserLogins"); 328 | }); 329 | 330 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 331 | { 332 | b.Property("UserId") 333 | .HasColumnType("nvarchar(450)"); 334 | 335 | b.Property("RoleId") 336 | .HasColumnType("nvarchar(450)"); 337 | 338 | b.HasKey("UserId", "RoleId"); 339 | 340 | b.HasIndex("RoleId"); 341 | 342 | b.ToTable("AspNetUserRoles"); 343 | }); 344 | 345 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 346 | { 347 | b.Property("UserId") 348 | .HasColumnType("nvarchar(450)"); 349 | 350 | b.Property("LoginProvider") 351 | .HasMaxLength(128) 352 | .HasColumnType("nvarchar(128)"); 353 | 354 | b.Property("Name") 355 | .HasMaxLength(128) 356 | .HasColumnType("nvarchar(128)"); 357 | 358 | b.Property("Value") 359 | .HasColumnType("nvarchar(max)"); 360 | 361 | b.HasKey("UserId", "LoginProvider", "Name"); 362 | 363 | b.ToTable("AspNetUserTokens"); 364 | }); 365 | 366 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 367 | { 368 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 369 | .WithMany() 370 | .HasForeignKey("RoleId") 371 | .OnDelete(DeleteBehavior.Cascade) 372 | .IsRequired(); 373 | }); 374 | 375 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 376 | { 377 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 378 | .WithMany() 379 | .HasForeignKey("UserId") 380 | .OnDelete(DeleteBehavior.Cascade) 381 | .IsRequired(); 382 | }); 383 | 384 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 385 | { 386 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 387 | .WithMany() 388 | .HasForeignKey("UserId") 389 | .OnDelete(DeleteBehavior.Cascade) 390 | .IsRequired(); 391 | }); 392 | 393 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 394 | { 395 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 396 | .WithMany() 397 | .HasForeignKey("RoleId") 398 | .OnDelete(DeleteBehavior.Cascade) 399 | .IsRequired(); 400 | 401 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 402 | .WithMany() 403 | .HasForeignKey("UserId") 404 | .OnDelete(DeleteBehavior.Cascade) 405 | .IsRequired(); 406 | }); 407 | 408 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 409 | { 410 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 411 | .WithMany() 412 | .HasForeignKey("UserId") 413 | .OnDelete(DeleteBehavior.Cascade) 414 | .IsRequired(); 415 | }); 416 | #pragma warning restore 612, 618 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /Data/Migrations/00000000000000_CreateIdentitySchema.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | 4 | namespace ASPNETReact.Data.Migrations 5 | { 6 | public partial class CreateIdentitySchema : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "AspNetRoles", 12 | columns: table => new 13 | { 14 | Id = table.Column(type: "nvarchar(450)", nullable: false), 15 | Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 16 | NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 17 | ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_AspNetRoles", x => x.Id); 22 | }); 23 | 24 | migrationBuilder.CreateTable( 25 | name: "AspNetUsers", 26 | columns: table => new 27 | { 28 | Id = table.Column(type: "nvarchar(450)", nullable: false), 29 | UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 30 | NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 31 | Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 32 | NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 33 | EmailConfirmed = table.Column(type: "bit", nullable: false), 34 | PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), 35 | SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), 36 | ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), 37 | PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), 38 | PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), 39 | TwoFactorEnabled = table.Column(type: "bit", nullable: false), 40 | LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), 41 | LockoutEnabled = table.Column(type: "bit", nullable: false), 42 | AccessFailedCount = table.Column(type: "int", nullable: false) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_AspNetUsers", x => x.Id); 47 | }); 48 | 49 | migrationBuilder.CreateTable( 50 | name: "DeviceCodes", 51 | columns: table => new 52 | { 53 | UserCode = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), 54 | DeviceCode = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), 55 | SubjectId = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), 56 | SessionId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), 57 | ClientId = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), 58 | Description = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), 59 | CreationTime = table.Column(type: "datetime2", nullable: false), 60 | Expiration = table.Column(type: "datetime2", nullable: false), 61 | Data = table.Column(type: "nvarchar(max)", maxLength: 52990, nullable: false) 62 | }, 63 | constraints: table => 64 | { 65 | table.PrimaryKey("PK_DeviceCodes", x => x.UserCode); 66 | }); 67 | 68 | migrationBuilder.CreateTable( 69 | name: "Keys", 70 | columns: table => new 71 | { 72 | Id = table.Column(type: "nvarchar(450)", nullable: false), 73 | Version = table.Column(type: "int", nullable: false), 74 | Created = table.Column(type: "datetime2", nullable: false), 75 | Use = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), 76 | Algorithm = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), 77 | IsX509Certificate = table.Column(type: "bit", nullable: false), 78 | DataProtected = table.Column(type: "bit", nullable: false), 79 | Data = table.Column(type: "nvarchar(max)", maxLength: 52990, nullable: false) 80 | }, 81 | constraints: table => 82 | { 83 | table.PrimaryKey("PK_Keys", x => x.Id); 84 | }); 85 | 86 | migrationBuilder.CreateTable( 87 | name: "PersistedGrants", 88 | columns: table => new 89 | { 90 | Key = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), 91 | Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), 92 | SubjectId = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), 93 | SessionId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), 94 | ClientId = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), 95 | Description = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), 96 | CreationTime = table.Column(type: "datetime2", nullable: false), 97 | Expiration = table.Column(type: "datetime2", nullable: true), 98 | ConsumedTime = table.Column(type: "datetime2", nullable: true), 99 | Data = table.Column(type: "nvarchar(max)", maxLength: 52990, nullable: false) 100 | }, 101 | constraints: table => 102 | { 103 | table.PrimaryKey("PK_PersistedGrants", x => x.Key); 104 | }); 105 | 106 | migrationBuilder.CreateTable( 107 | name: "AspNetRoleClaims", 108 | columns: table => new 109 | { 110 | Id = table.Column(type: "int", nullable: false) 111 | .Annotation("SqlServer:Identity", "1, 1"), 112 | RoleId = table.Column(type: "nvarchar(450)", nullable: false), 113 | ClaimType = table.Column(type: "nvarchar(max)", nullable: true), 114 | ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) 115 | }, 116 | constraints: table => 117 | { 118 | table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); 119 | table.ForeignKey( 120 | name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", 121 | column: x => x.RoleId, 122 | principalTable: "AspNetRoles", 123 | principalColumn: "Id", 124 | onDelete: ReferentialAction.Cascade); 125 | }); 126 | 127 | migrationBuilder.CreateTable( 128 | name: "AspNetUserClaims", 129 | columns: table => new 130 | { 131 | Id = table.Column(type: "int", nullable: false) 132 | .Annotation("SqlServer:Identity", "1, 1"), 133 | UserId = table.Column(type: "nvarchar(450)", nullable: false), 134 | ClaimType = table.Column(type: "nvarchar(max)", nullable: true), 135 | ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) 136 | }, 137 | constraints: table => 138 | { 139 | table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); 140 | table.ForeignKey( 141 | name: "FK_AspNetUserClaims_AspNetUsers_UserId", 142 | column: x => x.UserId, 143 | principalTable: "AspNetUsers", 144 | principalColumn: "Id", 145 | onDelete: ReferentialAction.Cascade); 146 | }); 147 | 148 | migrationBuilder.CreateTable( 149 | name: "AspNetUserLogins", 150 | columns: table => new 151 | { 152 | LoginProvider = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), 153 | ProviderKey = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), 154 | ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), 155 | UserId = table.Column(type: "nvarchar(450)", nullable: false) 156 | }, 157 | constraints: table => 158 | { 159 | table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); 160 | table.ForeignKey( 161 | name: "FK_AspNetUserLogins_AspNetUsers_UserId", 162 | column: x => x.UserId, 163 | principalTable: "AspNetUsers", 164 | principalColumn: "Id", 165 | onDelete: ReferentialAction.Cascade); 166 | }); 167 | 168 | migrationBuilder.CreateTable( 169 | name: "AspNetUserRoles", 170 | columns: table => new 171 | { 172 | UserId = table.Column(type: "nvarchar(450)", nullable: false), 173 | RoleId = table.Column(type: "nvarchar(450)", nullable: false) 174 | }, 175 | constraints: table => 176 | { 177 | table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); 178 | table.ForeignKey( 179 | name: "FK_AspNetUserRoles_AspNetRoles_RoleId", 180 | column: x => x.RoleId, 181 | principalTable: "AspNetRoles", 182 | principalColumn: "Id", 183 | onDelete: ReferentialAction.Cascade); 184 | table.ForeignKey( 185 | name: "FK_AspNetUserRoles_AspNetUsers_UserId", 186 | column: x => x.UserId, 187 | principalTable: "AspNetUsers", 188 | principalColumn: "Id", 189 | onDelete: ReferentialAction.Cascade); 190 | }); 191 | 192 | migrationBuilder.CreateTable( 193 | name: "AspNetUserTokens", 194 | columns: table => new 195 | { 196 | UserId = table.Column(type: "nvarchar(450)", nullable: false), 197 | LoginProvider = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), 198 | Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), 199 | Value = table.Column(type: "nvarchar(max)", nullable: true) 200 | }, 201 | constraints: table => 202 | { 203 | table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); 204 | table.ForeignKey( 205 | name: "FK_AspNetUserTokens_AspNetUsers_UserId", 206 | column: x => x.UserId, 207 | principalTable: "AspNetUsers", 208 | principalColumn: "Id", 209 | onDelete: ReferentialAction.Cascade); 210 | }); 211 | 212 | migrationBuilder.CreateIndex( 213 | name: "IX_AspNetRoleClaims_RoleId", 214 | table: "AspNetRoleClaims", 215 | column: "RoleId"); 216 | 217 | migrationBuilder.CreateIndex( 218 | name: "RoleNameIndex", 219 | table: "AspNetRoles", 220 | column: "NormalizedName", 221 | unique: true, 222 | filter: "[NormalizedName] IS NOT NULL"); 223 | 224 | migrationBuilder.CreateIndex( 225 | name: "IX_AspNetUserClaims_UserId", 226 | table: "AspNetUserClaims", 227 | column: "UserId"); 228 | 229 | migrationBuilder.CreateIndex( 230 | name: "IX_AspNetUserLogins_UserId", 231 | table: "AspNetUserLogins", 232 | column: "UserId"); 233 | 234 | migrationBuilder.CreateIndex( 235 | name: "IX_AspNetUserRoles_RoleId", 236 | table: "AspNetUserRoles", 237 | column: "RoleId"); 238 | 239 | migrationBuilder.CreateIndex( 240 | name: "EmailIndex", 241 | table: "AspNetUsers", 242 | column: "NormalizedEmail"); 243 | 244 | migrationBuilder.CreateIndex( 245 | name: "UserNameIndex", 246 | table: "AspNetUsers", 247 | column: "NormalizedUserName", 248 | unique: true, 249 | filter: "[NormalizedUserName] IS NOT NULL"); 250 | 251 | migrationBuilder.CreateIndex( 252 | name: "IX_DeviceCodes_DeviceCode", 253 | table: "DeviceCodes", 254 | column: "DeviceCode", 255 | unique: true); 256 | 257 | migrationBuilder.CreateIndex( 258 | name: "IX_DeviceCodes_Expiration", 259 | table: "DeviceCodes", 260 | column: "Expiration"); 261 | 262 | migrationBuilder.CreateIndex( 263 | name: "IX_Keys_Use", 264 | table: "Keys", 265 | column: "Use"); 266 | 267 | migrationBuilder.CreateIndex( 268 | name: "IX_PersistedGrants_ConsumedTime", 269 | table: "PersistedGrants", 270 | column: "ConsumedTime"); 271 | 272 | migrationBuilder.CreateIndex( 273 | name: "IX_PersistedGrants_Expiration", 274 | table: "PersistedGrants", 275 | column: "Expiration"); 276 | 277 | migrationBuilder.CreateIndex( 278 | name: "IX_PersistedGrants_SubjectId_ClientId_Type", 279 | table: "PersistedGrants", 280 | columns: new[] { "SubjectId", "ClientId", "Type" }); 281 | 282 | migrationBuilder.CreateIndex( 283 | name: "IX_PersistedGrants_SubjectId_SessionId_Type", 284 | table: "PersistedGrants", 285 | columns: new[] { "SubjectId", "SessionId", "Type" }); 286 | } 287 | 288 | protected override void Down(MigrationBuilder migrationBuilder) 289 | { 290 | migrationBuilder.DropTable( 291 | name: "AspNetRoleClaims"); 292 | 293 | migrationBuilder.DropTable( 294 | name: "AspNetUserClaims"); 295 | 296 | migrationBuilder.DropTable( 297 | name: "AspNetUserLogins"); 298 | 299 | migrationBuilder.DropTable( 300 | name: "AspNetUserRoles"); 301 | 302 | migrationBuilder.DropTable( 303 | name: "AspNetUserTokens"); 304 | 305 | migrationBuilder.DropTable( 306 | name: "DeviceCodes"); 307 | 308 | migrationBuilder.DropTable( 309 | name: "Keys"); 310 | 311 | migrationBuilder.DropTable( 312 | name: "PersistedGrants"); 313 | 314 | migrationBuilder.DropTable( 315 | name: "AspNetRoles"); 316 | 317 | migrationBuilder.DropTable( 318 | name: "AspNetUsers"); 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /Data/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using ASPNETReact.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using System; 8 | 9 | namespace ASPNETReact.Data.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .UseIdentityColumns() 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("ProductVersion", "5.0.0-rc.1.20417.2"); 21 | 22 | modelBuilder.Entity("ASPNETReact.Models.ApplicationUser", b => 23 | { 24 | b.Property("Id") 25 | .HasColumnType("nvarchar(450)"); 26 | 27 | b.Property("AccessFailedCount") 28 | .HasColumnType("int"); 29 | 30 | b.Property("ConcurrencyStamp") 31 | .IsConcurrencyToken() 32 | .HasColumnType("nvarchar(max)"); 33 | 34 | b.Property("Email") 35 | .HasMaxLength(256) 36 | .HasColumnType("nvarchar(256)"); 37 | 38 | b.Property("EmailConfirmed") 39 | .HasColumnType("bit"); 40 | 41 | b.Property("LockoutEnabled") 42 | .HasColumnType("bit"); 43 | 44 | b.Property("LockoutEnd") 45 | .HasColumnType("datetimeoffset"); 46 | 47 | b.Property("NormalizedEmail") 48 | .HasMaxLength(256) 49 | .HasColumnType("nvarchar(256)"); 50 | 51 | b.Property("NormalizedUserName") 52 | .HasMaxLength(256) 53 | .HasColumnType("nvarchar(256)"); 54 | 55 | b.Property("PasswordHash") 56 | .HasColumnType("nvarchar(max)"); 57 | 58 | b.Property("PhoneNumber") 59 | .HasColumnType("nvarchar(max)"); 60 | 61 | b.Property("PhoneNumberConfirmed") 62 | .HasColumnType("bit"); 63 | 64 | b.Property("SecurityStamp") 65 | .HasColumnType("nvarchar(max)"); 66 | 67 | b.Property("TwoFactorEnabled") 68 | .HasColumnType("bit"); 69 | 70 | b.Property("UserName") 71 | .HasMaxLength(256) 72 | .HasColumnType("nvarchar(256)"); 73 | 74 | b.HasKey("Id"); 75 | 76 | b.HasIndex("NormalizedEmail") 77 | .HasDatabaseName("EmailIndex"); 78 | 79 | b.HasIndex("NormalizedUserName") 80 | .IsUnique() 81 | .HasDatabaseName("UserNameIndex") 82 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 83 | 84 | b.ToTable("AspNetUsers"); 85 | }); 86 | 87 | modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b => 88 | { 89 | b.Property("UserCode") 90 | .HasMaxLength(200) 91 | .HasColumnType("nvarchar(200)"); 92 | 93 | b.Property("ClientId") 94 | .IsRequired() 95 | .HasMaxLength(200) 96 | .HasColumnType("nvarchar(200)"); 97 | 98 | b.Property("CreationTime") 99 | .HasColumnType("datetime2"); 100 | 101 | b.Property("Data") 102 | .IsRequired() 103 | .HasMaxLength(52990) 104 | .HasColumnType("nvarchar(max)"); 105 | 106 | b.Property("Description") 107 | .HasMaxLength(200) 108 | .HasColumnType("nvarchar(200)"); 109 | 110 | b.Property("DeviceCode") 111 | .IsRequired() 112 | .HasMaxLength(200) 113 | .HasColumnType("nvarchar(200)"); 114 | 115 | b.Property("Expiration") 116 | .IsRequired() 117 | .HasColumnType("datetime2"); 118 | 119 | b.Property("SessionId") 120 | .HasMaxLength(100) 121 | .HasColumnType("nvarchar(100)"); 122 | 123 | b.Property("SubjectId") 124 | .HasMaxLength(200) 125 | .HasColumnType("nvarchar(200)"); 126 | 127 | b.HasKey("UserCode"); 128 | 129 | b.HasIndex("DeviceCode") 130 | .IsUnique(); 131 | 132 | b.HasIndex("Expiration"); 133 | 134 | b.ToTable("DeviceCodes"); 135 | }); 136 | 137 | modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => 138 | { 139 | b.Property("Id") 140 | .HasMaxLength(450) 141 | .HasColumnType("nvarchar(450)"); 142 | 143 | b.Property("Algorithm") 144 | .IsRequired() 145 | .HasMaxLength(100) 146 | .HasColumnType("nvarchar(100)"); 147 | 148 | b.Property("Created") 149 | .HasColumnType("datetime2"); 150 | 151 | b.Property("Data") 152 | .IsRequired() 153 | .HasColumnType("nvarchar(max)") 154 | .HasMaxLength(52990); 155 | 156 | b.Property("DataProtected") 157 | .HasColumnType("bit"); 158 | 159 | b.Property("IsX509Certificate") 160 | .HasColumnType("bit"); 161 | 162 | b.Property("Use") 163 | .HasMaxLength(450) 164 | .HasColumnType("nvarchar(450)"); 165 | 166 | b.Property("Version") 167 | .HasColumnType("int"); 168 | 169 | b.HasKey("Id"); 170 | 171 | b.HasIndex("Use"); 172 | 173 | b.ToTable("Keys"); 174 | }); 175 | 176 | modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => 177 | { 178 | b.Property("Key") 179 | .HasMaxLength(200) 180 | .HasColumnType("nvarchar(200)"); 181 | 182 | b.Property("ClientId") 183 | .IsRequired() 184 | .HasMaxLength(200) 185 | .HasColumnType("nvarchar(200)"); 186 | 187 | b.Property("ConsumedTime") 188 | .HasColumnType("datetime2"); 189 | 190 | b.Property("CreationTime") 191 | .HasColumnType("datetime2"); 192 | 193 | b.Property("Data") 194 | .IsRequired() 195 | .HasMaxLength(52990) 196 | .HasColumnType("nvarchar(max)"); 197 | 198 | b.Property("Description") 199 | .HasMaxLength(200) 200 | .HasColumnType("nvarchar(200)"); 201 | 202 | b.Property("Expiration") 203 | .HasColumnType("datetime2"); 204 | 205 | b.Property("SessionId") 206 | .HasMaxLength(100) 207 | .HasColumnType("nvarchar(100)"); 208 | 209 | b.Property("SubjectId") 210 | .HasMaxLength(200) 211 | .HasColumnType("nvarchar(200)"); 212 | 213 | b.Property("Type") 214 | .IsRequired() 215 | .HasMaxLength(50) 216 | .HasColumnType("nvarchar(50)"); 217 | 218 | b.HasKey("Key"); 219 | 220 | b.HasIndex("Expiration"); 221 | 222 | b.HasIndex("SubjectId", "ClientId", "Type"); 223 | 224 | b.HasIndex("SubjectId", "SessionId", "Type"); 225 | 226 | b.ToTable("PersistedGrants"); 227 | }); 228 | 229 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 230 | { 231 | b.Property("Id") 232 | .HasColumnType("nvarchar(450)"); 233 | 234 | b.Property("ConcurrencyStamp") 235 | .IsConcurrencyToken() 236 | .HasColumnType("nvarchar(max)"); 237 | 238 | b.Property("Name") 239 | .HasMaxLength(256) 240 | .HasColumnType("nvarchar(256)"); 241 | 242 | b.Property("NormalizedName") 243 | .HasMaxLength(256) 244 | .HasColumnType("nvarchar(256)"); 245 | 246 | b.HasKey("Id"); 247 | 248 | b.HasIndex("NormalizedName") 249 | .IsUnique() 250 | .HasDatabaseName("RoleNameIndex") 251 | .HasFilter("[NormalizedName] IS NOT NULL"); 252 | 253 | b.ToTable("AspNetRoles"); 254 | }); 255 | 256 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 257 | { 258 | b.Property("Id") 259 | .ValueGeneratedOnAdd() 260 | .HasColumnType("int") 261 | .UseIdentityColumn(); 262 | 263 | b.Property("ClaimType") 264 | .HasColumnType("nvarchar(max)"); 265 | 266 | b.Property("ClaimValue") 267 | .HasColumnType("nvarchar(max)"); 268 | 269 | b.Property("RoleId") 270 | .IsRequired() 271 | .HasColumnType("nvarchar(450)"); 272 | 273 | b.HasKey("Id"); 274 | 275 | b.HasIndex("RoleId"); 276 | 277 | b.ToTable("AspNetRoleClaims"); 278 | }); 279 | 280 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 281 | { 282 | b.Property("Id") 283 | .ValueGeneratedOnAdd() 284 | .HasColumnType("int") 285 | .UseIdentityColumn(); 286 | 287 | b.Property("ClaimType") 288 | .HasColumnType("nvarchar(max)"); 289 | 290 | b.Property("ClaimValue") 291 | .HasColumnType("nvarchar(max)"); 292 | 293 | b.Property("UserId") 294 | .IsRequired() 295 | .HasColumnType("nvarchar(450)"); 296 | 297 | b.HasKey("Id"); 298 | 299 | b.HasIndex("UserId"); 300 | 301 | b.ToTable("AspNetUserClaims"); 302 | }); 303 | 304 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 305 | { 306 | b.Property("LoginProvider") 307 | .HasMaxLength(128) 308 | .HasColumnType("nvarchar(128)"); 309 | 310 | b.Property("ProviderKey") 311 | .HasMaxLength(128) 312 | .HasColumnType("nvarchar(128)"); 313 | 314 | b.Property("ProviderDisplayName") 315 | .HasColumnType("nvarchar(max)"); 316 | 317 | b.Property("UserId") 318 | .IsRequired() 319 | .HasColumnType("nvarchar(450)"); 320 | 321 | b.HasKey("LoginProvider", "ProviderKey"); 322 | 323 | b.HasIndex("UserId"); 324 | 325 | b.ToTable("AspNetUserLogins"); 326 | }); 327 | 328 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 329 | { 330 | b.Property("UserId") 331 | .HasColumnType("nvarchar(450)"); 332 | 333 | b.Property("RoleId") 334 | .HasColumnType("nvarchar(450)"); 335 | 336 | b.HasKey("UserId", "RoleId"); 337 | 338 | b.HasIndex("RoleId"); 339 | 340 | b.ToTable("AspNetUserRoles"); 341 | }); 342 | 343 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 344 | { 345 | b.Property("UserId") 346 | .HasColumnType("nvarchar(450)"); 347 | 348 | b.Property("LoginProvider") 349 | .HasMaxLength(128) 350 | .HasColumnType("nvarchar(128)"); 351 | 352 | b.Property("Name") 353 | .HasMaxLength(128) 354 | .HasColumnType("nvarchar(128)"); 355 | 356 | b.Property("Value") 357 | .HasColumnType("nvarchar(max)"); 358 | 359 | b.HasKey("UserId", "LoginProvider", "Name"); 360 | 361 | b.ToTable("AspNetUserTokens"); 362 | }); 363 | 364 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 365 | { 366 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 367 | .WithMany() 368 | .HasForeignKey("RoleId") 369 | .OnDelete(DeleteBehavior.Cascade) 370 | .IsRequired(); 371 | }); 372 | 373 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 374 | { 375 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 376 | .WithMany() 377 | .HasForeignKey("UserId") 378 | .OnDelete(DeleteBehavior.Cascade) 379 | .IsRequired(); 380 | }); 381 | 382 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 383 | { 384 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 385 | .WithMany() 386 | .HasForeignKey("UserId") 387 | .OnDelete(DeleteBehavior.Cascade) 388 | .IsRequired(); 389 | }); 390 | 391 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 392 | { 393 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 394 | .WithMany() 395 | .HasForeignKey("RoleId") 396 | .OnDelete(DeleteBehavior.Cascade) 397 | .IsRequired(); 398 | 399 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 400 | .WithMany() 401 | .HasForeignKey("UserId") 402 | .OnDelete(DeleteBehavior.Cascade) 403 | .IsRequired(); 404 | }); 405 | 406 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 407 | { 408 | b.HasOne("ASPNETReact.Models.ApplicationUser", null) 409 | .WithMany() 410 | .HasForeignKey("UserId") 411 | .OnDelete(DeleteBehavior.Cascade) 412 | .IsRequired(); 413 | }); 414 | #pragma warning restore 612, 618 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace ASPNETReact.Models 4 | { 5 | public class ApplicationUser : IdentityUser 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

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

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

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

21 |

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

27 | -------------------------------------------------------------------------------- /Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | 5 | namespace ASPNETReact.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | public class ErrorModel : PageModel 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public ErrorModel(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public string? RequestId { get; set; } 18 | 19 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 20 | 21 | public void OnGet() 22 | { 23 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using ASPNETReact.Models; 3 | @inject SignInManager SignInManager 4 | @inject UserManager UserManager 5 | 6 | @{ 7 | string? returnUrl = null; 8 | var query = ViewContext.HttpContext.Request.Query; 9 | if (query.ContainsKey("returnUrl")) 10 | { 11 | returnUrl = query["returnUrl"]; 12 | } 13 | } 14 | 15 | 37 | -------------------------------------------------------------------------------- /Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using ASPNETReact 2 | @namespace ASPNETReact.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using ASPNETReact.Data; 2 | using ASPNETReact.Models; 3 | using Microsoft.AspNetCore.Authentication; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.AspNetCore.Identity.UI; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | 10 | // Add services to the container. 11 | var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); 12 | builder.Services.AddDbContext(options => 13 | options.UseSqlServer(connectionString)); 14 | builder.Services.AddDatabaseDeveloperPageExceptionFilter(); 15 | 16 | builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) 17 | .AddEntityFrameworkStores(); 18 | 19 | builder.Services.AddIdentityServer() 20 | .AddApiAuthorization(); 21 | 22 | builder.Services.AddAuthentication() 23 | .AddIdentityServerJwt(); 24 | 25 | builder.Services.AddControllersWithViews(); 26 | builder.Services.AddRazorPages(); 27 | 28 | var app = builder.Build(); 29 | 30 | // Configure the HTTP request pipeline. 31 | if (app.Environment.IsDevelopment()) 32 | { 33 | app.UseMigrationsEndPoint(); 34 | } 35 | else 36 | { 37 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 38 | app.UseHsts(); 39 | } 40 | 41 | app.UseHttpsRedirection(); 42 | app.UseStaticFiles(); 43 | app.UseRouting(); 44 | 45 | app.UseAuthentication(); 46 | app.UseIdentityServer(); 47 | app.UseAuthorization(); 48 | 49 | app.MapControllerRoute( 50 | name: "default", 51 | pattern: "{controller}/{action=Index}/{id?}"); 52 | app.MapRazorPages(); 53 | 54 | app.MapFallbackToFile("index.html"); ; 55 | 56 | app.Run(); 57 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:65469", 7 | "sslPort": 44398 8 | } 9 | }, 10 | "profiles": { 11 | "ASPNETReact": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "applicationUrl": "https://localhost:7299;http://localhost:5299", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development", 17 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development", 25 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql", 5 | "connectionId": "ConnectionStrings:DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql.local", 5 | "connectionId": "ConnectionStrings:DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace ASPNETReact 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 | } -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.AspNetCore.SpaProxy": "Information", 7 | "Microsoft.Hosting.Lifetime": "Information" 8 | } 9 | }, 10 | "IdentityServer": { 11 | "Key": { 12 | "Type": "Development" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-ASPNETReact-53bc9b9d-9d6a-45d4-8429-2a2761773502;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | }, 12 | "IdentityServer": { 13 | "Clients": { 14 | "ASPNETReact": { 15 | "Profile": "IdentityServerSPA" 16 | } 17 | } 18 | }, 19 | "AllowedHosts": "*" 20 | } 21 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ASPNETReact", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | --------------------------------------------------------------------------------