├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml ├── docs ├── Images │ ├── Skoruba-Logo-ReadMe.png │ └── react-oidc-client-screenshot.png └── README.md └── src ├── .gitignore ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json ├── oidc-client.min.js ├── signin-callback.html └── silent-renew.html ├── src ├── components │ ├── AppContent.tsx │ ├── AuthContent.tsx │ ├── Buttons.test.tsx │ ├── Buttons.tsx │ ├── Header.test.tsx │ ├── Header.tsx │ ├── JsonTreeViewer.tsx │ └── __snapshots__ │ │ ├── Buttons.test.tsx.snap │ │ └── Header.test.tsx.snap ├── containers │ ├── App.css │ ├── App.test.tsx │ └── App.tsx ├── helpers │ └── Constants.ts ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── registerServiceWorker.ts ├── services │ ├── ApiService.ts │ └── AuthService.ts └── setupTests.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jan Škoruba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](docs/Images/Skoruba-Logo-ReadMe.png) 2 | 3 | # React-oidc-client-js 4 | 5 | > OpenID Connect (OIDC) client with React and typescript 6 | 7 | - This is sample application that contains [oidc-client-js](https://github.com/IdentityModel/oidc-client-js) and `React` with `Typescript`. 8 | 9 | - The application is based on `create-react-app` - [Create React App](https://github.com/facebook/create-react-app) 10 | 11 | # Project status 12 | [![Build status](https://ci.appveyor.com/api/projects/status/5ml2f07trcm072a1?svg=true)](https://ci.appveyor.com/project/JanSkoruba/react-oidc-client-js) 13 | 14 | # Installation 15 | 16 | ## Cloning app 17 | 18 | - `git clone https://github.com/skoruba/react-oidc-client-js.git` 19 | - `cd src/` 20 | 21 | ## Install dependecies 22 | 23 | - Install dependecies - `yarn install` 24 | 25 | ## Running app 26 | 27 | - `yarn start` - start the web server that is running on [http://localhost:4200](http://localhost:4200) 28 | 29 | - The application is connected to `OpenID Connect Provider` that is running on [https://demo.identityserver.io/](https://demo.identityserver.io/) 30 | - This STS has configured a SPA client to run on http://localhost:4200, therefore a sample application will be running on this port `4200`. 31 | 32 | ## App preview 33 | 34 | ![Logo](docs/Images/react-oidc-client-screenshot.png) 35 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '12' 4 | - nodejs_version: '10' 5 | - nodejs_version: '8' 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - npm install --global npm@latest 9 | - set PATH=%APPDATA%\npm;%PATH% 10 | - npm install -g create-react-app 11 | - npm install 12 | matrix: 13 | fast_finish: true 14 | build: off 15 | shallow_clone: true 16 | test_script: 17 | - node --version 18 | - npm --version 19 | - cd src/ 20 | - npm install -g create-react-app 21 | - npm install 22 | - npm test 23 | cache: 24 | - '%APPDATA%\npm-cache' 25 | -------------------------------------------------------------------------------- /docs/Images/Skoruba-Logo-ReadMe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skoruba/react-oidc-client-js/76fd43b46c565ec7b8869d1e97879cf737aba572/docs/Images/Skoruba-Logo-ReadMe.png -------------------------------------------------------------------------------- /docs/Images/react-oidc-client-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skoruba/react-oidc-client-js/76fd43b46c565ec7b8869d1e97879cf737aba572/docs/Images/react-oidc-client-screenshot.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ![Logo](Images/Skoruba-Logo-ReadMe.png) 2 | 3 | # react-oidc-client-js docs 4 | -------------------------------------------------------------------------------- /src/.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 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-oidc-client-js", 3 | "description": "OpenID Connect (OIDC) javascript client with React", 4 | "version": "3.0.0", 5 | "dependencies": { 6 | "axios": "0.19.2", 7 | "core-js": "3.6.5", 8 | "bootstrap": "4.5.0", 9 | "enzyme": "3.11.0", 10 | "enzyme-adapter-react-16": "1.15.2", 11 | "oidc-client": "1.10.1", 12 | "ramda": "0.27.0", 13 | "react": "16.13.1", 14 | "react-dom": "16.13.1", 15 | "react-json-tree": "0.11.2", 16 | "react-scripts": "3.4.1", 17 | "react-test-renderer": "16.13.1", 18 | "react-app-polyfill": "1.0.6", 19 | "react-toastify": "6.0.8" 20 | }, 21 | "scripts": { 22 | "start": "set PORT=4200 && react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | }, 27 | "devDependencies": { 28 | "@types/axios": "0.14.0", 29 | "@types/enzyme": "3.10.5", 30 | "@types/enzyme-adapter-react-16": "1.0.6", 31 | "@types/jest": "26.0.4", 32 | "@types/node": "14.0.23", 33 | "@types/ramda": "0.27.11", 34 | "@types/react": "16.9.43", 35 | "@types/react-dom": "16.9.8", 36 | "@types/react-json-tree": "0.6.11", 37 | "@types/react-test-renderer": "16.9.2", 38 | "@types/toastr": "2.1.38", 39 | "typescript": "3.9.6" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version", 51 | "ie 11" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skoruba/react-oidc-client-js/76fd43b46c565ec7b8869d1e97879cf737aba572/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 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 | -------------------------------------------------------------------------------- /src/public/signin-callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Authentification callback processing.. 6 | 7 | 8 | 9 | 12 | 13 |

Authentification callback processing...

14 | 15 | 16 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/public/silent-renew.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/src/components/AppContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ToastContainer, toast } from 'react-toastify'; 4 | import { ApiService } from '../services/ApiService'; 5 | import { AuthService } from '../services/AuthService'; 6 | 7 | import AuthContent from './AuthContent'; 8 | import Buttons from './Buttons'; 9 | 10 | export default class AppContent extends React.Component { 11 | public authService: AuthService; 12 | public apiService: ApiService; 13 | private shouldCancel: boolean; 14 | 15 | constructor(props: any) { 16 | super(props); 17 | 18 | this.authService = new AuthService(); 19 | this.apiService = new ApiService(); 20 | this.state = { user: {}, api: {} }; 21 | this.shouldCancel = false; 22 | } 23 | 24 | public componentDidMount() { 25 | this.getUser(); 26 | } 27 | 28 | public login = () => { 29 | this.authService.login(); 30 | }; 31 | 32 | public callApi = () => { 33 | this.apiService 34 | .callApi() 35 | .then(data => { 36 | this.setState({ api: data.data }); 37 | toast.success('Api return successfully data, check in section - Api response'); 38 | }) 39 | .catch(error => { 40 | toast.error(error); 41 | }); 42 | }; 43 | 44 | public componentWillUnmount() { 45 | this.shouldCancel = true; 46 | } 47 | 48 | public renewToken = () => { 49 | this.authService 50 | .renewToken() 51 | .then(user => { 52 | toast.success('Token has been sucessfully renewed. :-)'); 53 | this.getUser(); 54 | }) 55 | .catch(error => { 56 | toast.error(error); 57 | }); 58 | }; 59 | 60 | public logout = () => { 61 | this.authService.logout(); 62 | }; 63 | 64 | public getUser = () => { 65 | this.authService.getUser().then(user => { 66 | if (user) { 67 | toast.success('User has been successfully loaded from store.'); 68 | } else { 69 | toast.info('You are not logged in.'); 70 | } 71 | 72 | if (!this.shouldCancel) { 73 | this.setState({ user }); 74 | } 75 | }); 76 | }; 77 | 78 | public render() { 79 | return ( 80 | <> 81 | 82 | 83 | 90 | 91 | 92 | 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/src/components/AuthContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import JsonTreeViewer from './JsonTreeViewer'; 3 | 4 | export interface IAuthContentProps { 5 | api: any; 6 | user: any; 7 | } 8 | 9 | export default class AuthContent extends React.Component { 10 | public shouldExpandNode = (keyPath: Array, data: [any] | {}, level: number) => { 11 | return true; 12 | }; 13 | 14 | public render() { 15 | return ( 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/src/components/Buttons.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import * as React from 'react'; 3 | import * as renderer from 'react-test-renderer'; 4 | 5 | import Buttons from './Buttons'; 6 | 7 | describe(Buttons, () => { 8 | const mockLogin = jest.fn(); 9 | const mockLogout = jest.fn(); 10 | const mockRenewToken = jest.fn(); 11 | const mockCallApi = jest.fn(); 12 | const mockGetUser = jest.fn(); 13 | 14 | const component = shallow( 15 | 22 | ); 23 | 24 | it('renders and matches our snapshot', () => { 25 | const buttonComponent = renderer.create( 26 | 33 | ); 34 | const tree = buttonComponent.toJSON(); 35 | expect(tree).toMatchSnapshot(); 36 | }); 37 | 38 | it('contains the image and h1', () => { 39 | expect(component.find('button')).toHaveLength(5); 40 | }); 41 | 42 | it('try to click on login button', () => { 43 | component.find('button.btn-login').simulate('click'); 44 | expect(mockLogin).toBeCalled(); 45 | }); 46 | 47 | it('try to click on logout button', () => { 48 | component.find('button.btn-logout').simulate('click'); 49 | expect(mockLogout).toBeCalled(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/src/components/Buttons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IButtonsProps { 4 | login: () => void; 5 | getUser: () => void; 6 | callApi: () => void; 7 | renewToken: () => void; 8 | logout: () => void; 9 | } 10 | 11 | const Buttons: React.SFC = props => { 12 | return ( 13 |
14 |
15 | 18 | 21 | 24 | 27 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Buttons; 36 | -------------------------------------------------------------------------------- /src/src/components/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import * as React from 'react'; 3 | import * as renderer from 'react-test-renderer'; 4 | 5 | import logo from '../logo.svg'; 6 | import Header from './Header'; 7 | 8 | describe(Header, () => { 9 | const pageTitle = 'React app with oidc-client-js'; 10 | 11 | const component = shallow(
); 12 | 13 | it('renders and matches our snapshot', () => { 14 | const headerComponent = renderer.create(
); 15 | const tree = headerComponent.toJSON(); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | 19 | it('contains the image and h1', () => { 20 | expect(component.find('img')).toHaveLength(1); 21 | expect(component.find('h1')).toHaveLength(1); 22 | }); 23 | 24 | it('h1 contains correct title', () => { 25 | expect(component.find('h1').text()).toEqual(pageTitle); 26 | expect(component.contains(pageTitle)).toEqual(true); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface IHeaderProps { 4 | pageTitle: string; 5 | logoSrc: any; 6 | } 7 | 8 | export default function Header(props: IHeaderProps) { 9 | return ( 10 |
11 | logo 12 |

{props.pageTitle}

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/src/components/JsonTreeViewer.tsx: -------------------------------------------------------------------------------- 1 | import * as R from 'ramda'; 2 | import * as React from 'react'; 3 | import JSONTree from 'react-json-tree'; 4 | 5 | export interface IJsonTreeViewer { 6 | data: any; 7 | title: string; 8 | shouldExpandNode?: (keyPath: Array, data: [any] | {}, level: number) => boolean; 9 | } 10 | 11 | export default class JsonTreeViewer extends React.Component { 12 | public renderJsonData() { 13 | return R.not(R.isEmpty(this.props.data)) && R.not(R.isNil(this.props.data)) ? ( 14 | <> 15 |

{this.props.title}

16 | 17 | 18 | ) : null; 19 | } 20 | 21 | public render() { 22 | return this.renderJsonData(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/src/components/__snapshots__/Buttons.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Buttons renders and matches our snapshot 1`] = ` 4 |
7 |
15 | 26 | 37 | 48 | 59 | 70 |
71 |
72 | `; 73 | -------------------------------------------------------------------------------- /src/src/components/__snapshots__/Header.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Header renders and matches our snapshot 1`] = ` 4 |
7 | logo 12 |

15 | React app with oidc-client-js 16 |

17 |
18 | `; 19 | -------------------------------------------------------------------------------- /src/src/containers/App.css: -------------------------------------------------------------------------------- 1 | .App-logo { 2 | animation: App-logo-spin infinite 20s linear; 3 | height: 80px; 4 | } 5 | 6 | .App-header { 7 | background-color: #222; 8 | height: 150px; 9 | padding: 20px; 10 | color: white; 11 | text-align: center; 12 | } 13 | 14 | .App-title { 15 | font-size: 1.5em; 16 | } 17 | 18 | .App-intro { 19 | font-size: large; 20 | } 21 | 22 | @keyframes App-logo-spin { 23 | from { 24 | transform: rotate(0deg); 25 | } 26 | to { 27 | transform: rotate(360deg); 28 | } 29 | } 30 | 31 | #toast-container > div { 32 | opacity: 1; 33 | } 34 | -------------------------------------------------------------------------------- /src/src/containers/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | const storageMock = { 6 | clear: jest.fn(), 7 | getItem: jest.fn(), 8 | setItem: jest.fn() 9 | }; 10 | 11 | (global as any).sessionStorage = storageMock; 12 | (global as any).localStorage = storageMock; 13 | 14 | it('renders without crashing', () => { 15 | const div = document.createElement('div'); 16 | ReactDOM.render(, div); 17 | ReactDOM.unmountComponentAtNode(div); 18 | }); 19 | -------------------------------------------------------------------------------- /src/src/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import AppContent from '../components/AppContent'; 3 | import Header from '../components/Header'; 4 | import logo from '../logo.svg'; 5 | import './App.css'; 6 | 7 | class App extends React.Component { 8 | public render() { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 | ); 21 | } 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /src/src/helpers/Constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | public static stsAuthority = 'https://demo.identityserver.io/'; 3 | public static clientId = 'interactive.public'; 4 | public static clientRoot = 'http://localhost:4200/'; 5 | public static clientScope = 'openid profile email api'; 6 | 7 | public static apiRoot = 'https://demo.identityserver.io/api/'; 8 | } 9 | -------------------------------------------------------------------------------- /src/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /src/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.min.css'; 2 | import 'react-toastify/dist/ReactToastify.css'; 3 | 4 | import 'react-app-polyfill/ie11'; 5 | import 'core-js'; 6 | 7 | import * as React from 'react'; 8 | import * as ReactDOM from 'react-dom'; 9 | import App from './containers/App'; 10 | import './index.css'; 11 | import registerServiceWorker from './registerServiceWorker'; 12 | 13 | ReactDOM.render(, document.getElementById('root') as HTMLElement); 14 | registerServiceWorker(); 15 | -------------------------------------------------------------------------------- /src/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-console 2 | // In production, we register a service worker to serve assets from local cache. 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 the 'N+1' visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === 'localhost' || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === '[::1]' || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL( 26 | process.env.PUBLIC_URL!, 27 | window.location.toString() 28 | ); 29 | if (publicUrl.origin !== window.location.origin) { 30 | // Our service worker won't work if PUBLIC_URL is on a different origin 31 | // from what our page is served on. This might happen if a CDN is used to 32 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 33 | return; 34 | } 35 | 36 | window.addEventListener('load', () => { 37 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 38 | 39 | if (isLocalhost) { 40 | // This is running on localhost. Lets check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl); 42 | 43 | // Add some additional logging to localhost, pointing developers to the 44 | // service worker/PWA documentation. 45 | navigator.serviceWorker.ready.then(() => { 46 | console.log( 47 | 'This web app is being served cache-first by a service ' + 48 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 49 | ); 50 | }); 51 | } else { 52 | // Is not local host. Just register service worker 53 | registerValidSW(swUrl); 54 | } 55 | }); 56 | } 57 | } 58 | 59 | function registerValidSW(swUrl: string) { 60 | navigator.serviceWorker 61 | .register(swUrl) 62 | .then(registration => { 63 | registration.onupdatefound = () => { 64 | const installingWorker = registration.installing; 65 | if (installingWorker) { 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the old content will have been purged and 70 | // the fresh content will have been added to the cache. 71 | // It's the perfect time to display a 'New content is 72 | // available; please refresh.' message in your web app. 73 | console.log('New content is available; please refresh.'); 74 | } else { 75 | // At this point, everything has been precached. 76 | // It's the perfect time to display a 77 | // 'Content is cached for offline use.' message. 78 | console.log('Content is cached for offline use.'); 79 | } 80 | } 81 | }; 82 | } 83 | }; 84 | }) 85 | .catch(error => { 86 | console.error('Error during service worker registration:', error); 87 | }); 88 | } 89 | 90 | function checkValidServiceWorker(swUrl: string) { 91 | // Check if the service worker can be found. If it can't reload the page. 92 | fetch(swUrl) 93 | .then(response => { 94 | // Ensure service worker exists, and that we really are getting a JS file. 95 | if ( 96 | response.status === 404 || 97 | response.headers.get('content-type')!.indexOf('javascript') === -1 98 | ) { 99 | // No service worker found. Probably a different app. Reload the page. 100 | navigator.serviceWorker.ready.then(registration => { 101 | registration.unregister().then(() => { 102 | window.location.reload(); 103 | }); 104 | }); 105 | } else { 106 | // Service worker found. Proceed as normal. 107 | registerValidSW(swUrl); 108 | } 109 | }) 110 | .catch(() => { 111 | console.log( 112 | 'No internet connection found. App is running in offline mode.' 113 | ); 114 | }); 115 | } 116 | 117 | export function unregister() { 118 | if ('serviceWorker' in navigator) { 119 | navigator.serviceWorker.ready.then(registration => { 120 | registration.unregister(); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/src/services/ApiService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Constants } from '../helpers/Constants'; 3 | import { AuthService } from './AuthService'; 4 | 5 | export class ApiService { 6 | private authService: AuthService; 7 | 8 | constructor() { 9 | this.authService = new AuthService(); 10 | } 11 | 12 | public callApi(): Promise { 13 | return this.authService.getUser().then(user => { 14 | if (user && user.access_token) { 15 | return this._callApi(user.access_token).catch(error => { 16 | if (error.response.status === 401) { 17 | return this.authService.renewToken().then(renewedUser => { 18 | return this._callApi(renewedUser.access_token); 19 | }); 20 | } 21 | throw error; 22 | }); 23 | } else if (user) { 24 | return this.authService.renewToken().then(renewedUser => { 25 | return this._callApi(renewedUser.access_token); 26 | }); 27 | } else { 28 | throw new Error('user is not logged in'); 29 | } 30 | }); 31 | } 32 | 33 | private _callApi(token: string) { 34 | const headers = { 35 | Accept: 'application/json', 36 | Authorization: 'Bearer ' + token 37 | }; 38 | 39 | return axios.get(Constants.apiRoot + 'test', { headers }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/src/services/AuthService.ts: -------------------------------------------------------------------------------- 1 | import { Log, User, UserManager } from 'oidc-client'; 2 | 3 | import { Constants } from '../helpers/Constants'; 4 | 5 | export class AuthService { 6 | public userManager: UserManager; 7 | 8 | constructor() { 9 | const settings = { 10 | authority: Constants.stsAuthority, 11 | client_id: Constants.clientId, 12 | redirect_uri: `${Constants.clientRoot}signin-callback.html`, 13 | silent_redirect_uri: `${Constants.clientRoot}silent-renew.html`, 14 | // tslint:disable-next-line:object-literal-sort-keys 15 | post_logout_redirect_uri: `${Constants.clientRoot}`, 16 | response_type: 'code', 17 | scope: Constants.clientScope 18 | }; 19 | this.userManager = new UserManager(settings); 20 | 21 | Log.logger = console; 22 | Log.level = Log.INFO; 23 | } 24 | 25 | public getUser(): Promise { 26 | return this.userManager.getUser(); 27 | } 28 | 29 | public login(): Promise { 30 | return this.userManager.signinRedirect(); 31 | } 32 | 33 | public renewToken(): Promise { 34 | return this.userManager.signinSilent(); 35 | } 36 | 37 | public logout(): Promise { 38 | return this.userManager.signoutRedirect(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], 3 | "linterOptions": { 4 | "exclude": [ 5 | "config/**/*.js", 6 | "node_modules/**/*.ts" 7 | ] 8 | } 9 | } 10 | --------------------------------------------------------------------------------