├── .gitignore
├── client
├── app
│ ├── styles
│ │ ├── textarea.css
│ │ ├── font.css
│ │ ├── spacing.css
│ │ ├── layout.css
│ │ └── color.css
│ ├── index.css
│ ├── index.html
│ ├── components
│ │ ├── Response.js
│ │ ├── Greeting.js
│ │ ├── LogInOut.js
│ │ └── UserData.js
│ └── index.js
├── webpack.config.js
└── package.json
├── server
├── .gitignore
├── routes
│ ├── logout.js
│ ├── login.js
│ ├── oauth-callback.js
│ ├── set-user-data.js
│ └── user.js
├── package.json
├── helpers
│ └── pkce.js
├── index.js
└── package-lock.json
├── .env
├── .github
└── workflows
│ └── main.yaml
├── config.js
├── docker-compose.yml
├── kickstart
└── kickstart.json
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.iws
3 |
--------------------------------------------------------------------------------
/client/app/styles/textarea.css:
--------------------------------------------------------------------------------
1 | #UserData textarea {
2 | resize: none;
3 | }
4 |
5 | #UserData textarea:read-only {
6 | cursor: not-allowed;
7 | }
8 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Node
4 | /node_modules
5 | npm-debug.log
6 | yarn-error.log
7 |
--------------------------------------------------------------------------------
/client/app/index.css:
--------------------------------------------------------------------------------
1 | @import './styles/color.css';
2 | @import './styles/font.css';
3 | @import './styles/layout.css';
4 | @import './styles/spacing.css';
5 | @import './styles/textarea.css';
6 |
--------------------------------------------------------------------------------
/client/app/styles/font.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | }
4 |
5 | header h1 {
6 | font-size: 24px;
7 | }
8 |
9 | main,
10 | #UserData textarea {
11 | font-size: 12px;
12 | }
13 |
14 | header a {
15 | text-decoration: none;
16 | }
17 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=postgres
2 | POSTGRES_PASSWORD=postgres
3 | DATABASE_USERNAME=fusionauth
4 | DATABASE_PASSWORD=hkaLBM3RVnyYeYeqE3WI1w2e4Avpy0Wd5O3s3
5 | ES_JAVA_OPTS="-Xms512m -Xmx512m"
6 | FUSIONAUTH_APP_MEMORY=512M
7 |
8 | FUSIONAUTH_APP_KICKSTART_FILE=/usr/local/fusionauth/kickstart/kickstart.json
9 |
--------------------------------------------------------------------------------
/client/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Fusionauth Example React
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/app/components/Response.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class Response extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | }
7 |
8 | render() {
9 | return (
10 |
11 |
FusionAuth Response
12 |
{JSON.stringify(this.props.body, null, '\t')}
13 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/app/components/Greeting.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class Greeting extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | }
7 |
8 | render() {
9 | let message = (this.props.body.tid)
10 | ? `Hi, ${this.props.body.email}!`
11 | : "You're not logged in.";
12 |
13 | return (
14 | {message}
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/server/routes/logout.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const config = require('../../config');
4 |
5 | router.get('/', (req, res) => {
6 | // delete the session
7 | req.session.destroy();
8 |
9 | // end FusionAuth session
10 | res.redirect(`http://localhost:${config.fusionAuthPort}/oauth2/logout?client_id=${config.clientID}`);
11 | });
12 |
13 | module.exports = router;
14 |
15 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | # This is a starting workflow for building with GitHub Actions
2 | name: Build
3 |
4 | on:
5 | push:
6 | branches: [ master, main ]
7 | pull_request:
8 | branches: [ master, main ]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | # Check out code
16 | - uses: actions/checkout@v3
17 |
18 | # Set up the build environment
19 |
20 | # Build
21 |
22 | # Done!
23 |
--------------------------------------------------------------------------------
/client/app/components/LogInOut.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class LogInOut extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | }
7 |
8 | render() {
9 | console.log("this.props.body", this.props.body);
10 | let message = (this.props.body.tid)
11 | ? 'sign out'
12 | : 'sign in';
13 |
14 | let path = (this.props.body.tid)
15 | ? '/logout'
16 | : '/login';
17 |
18 | return (
19 | {message}
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "Secure-React server",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node index.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "cors": "^2.8.5",
14 | "express": "^4.18.2",
15 | "express-session": "^1.17.3",
16 | "request": "^2.88.2",
17 | "rxjs": "^7.5.7",
18 | "tslib": "^2.4.1",
19 | "zone.js": "^0.11.8"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/server/helpers/pkce.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 |
3 | function base64URLEncode(str) {
4 | return str
5 | .toString("base64")
6 | .replace(/\+/g, "-")
7 | .replace(/\//g, "_")
8 | .replace(/=/g, "")
9 | }
10 |
11 | function sha256(buffer) {
12 | return crypto.createHash("sha256").update(buffer).digest()
13 | }
14 |
15 | module.exports.generateVerifier = () => {
16 | return base64URLEncode(crypto.randomBytes(32))
17 | }
18 |
19 | module.exports.generateChallenge = (verifier) => {
20 | return base64URLEncode(sha256(verifier))
21 | }
22 |
--------------------------------------------------------------------------------
/client/app/styles/spacing.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | }
4 |
5 | #App {
6 | height: 100vh;
7 | }
8 |
9 | header,
10 | main,
11 | footer {
12 | padding: 20px 5vw;
13 | }
14 |
15 | header h1 {
16 | margin: 0;
17 | }
18 |
19 | header span {
20 | margin: 10px 20px 10px 0;
21 | }
22 |
23 | header a {
24 | padding: 5px 10px;
25 | }
26 |
27 | footer a {
28 | margin: 0 20px 0 0;
29 | }
30 |
31 | #UserData,
32 | #Response {
33 | height: 100%;
34 | width: 100%;
35 | }
36 |
37 | #Response pre,
38 | #UserData textarea {
39 | padding: 5px 10px;
40 | margin: 0;
41 | }
42 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |
3 | // These values must match what's in the kickstart/kickstart.json file
4 | apiKey: 'this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works_dont_use_this_in_prod',
5 | applicationID: 'E9FDB985-9173-4E01-9D73-AC2D60D1DC8E',
6 | clientID: 'E9FDB985-9173-4E01-9D73-AC2D60D1DC8E',
7 | clientSecret: 'super-secret-secret-that-should-be-regenerated-for-production',
8 | redirectURI: 'http://localhost:3000/oauth-callback',
9 |
10 | //ports
11 | clientPort: 4200,
12 | serverPort: 3000,
13 | fusionAuthPort: 9011
14 | };
15 |
16 |
--------------------------------------------------------------------------------
/client/app/styles/layout.css:
--------------------------------------------------------------------------------
1 | #App,
2 | main,
3 | #UserData,
4 | #Response {
5 | display: flex;
6 | flex-direction: column;
7 | }
8 |
9 | header,
10 | main {
11 | display: flex;
12 | align-items: center;
13 | justify-content: space-between;
14 | }
15 |
16 | @media (max-width: 750px) {
17 | header {
18 | flex-direction: column;
19 | align-items: flex-start;
20 | }
21 | }
22 |
23 | #UserData {
24 | flex-basis: 20%;
25 | }
26 |
27 | #Response {
28 | flex-basis: 80%;
29 | }
30 |
31 | header h1,
32 | main,
33 | #UserData,
34 | #Response,
35 | #Response pre,
36 | #UserData textarea {
37 | flex-grow: 1;
38 | }
39 |
--------------------------------------------------------------------------------
/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './app/index.js',
6 | output: {
7 | path: path.resolve(__dirname, 'dist'),
8 | filename: 'index_bundle.js'
9 | },
10 | module: {
11 | rules: [
12 | {
13 | test: /\.(js)$/,
14 | use: 'babel-loader'
15 | },
16 | {
17 | test: /\.css$/,
18 | use: ['style-loader', 'css-loader']
19 | }
20 | ]
21 | },
22 | mode: 'development',
23 | plugins: [
24 | new HtmlWebpackPlugin({
25 | template: 'app/index.html'
26 | })
27 | ]
28 | };
29 |
--------------------------------------------------------------------------------
/server/routes/login.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const config = require('../../config');
4 | const pkce = require('../helpers/pkce');
5 |
6 | router.get('/', (req, res) => {
7 | // Generate and store the PKCE verifier
8 | req.session.verifier = pkce.generateVerifier();
9 |
10 | // Generate the PKCE challenge
11 | const challenge = pkce.generateChallenge(req.session.verifier);
12 |
13 | res.redirect(`http://localhost:${config.fusionAuthPort}/oauth2/authorize?client_id=${config.clientID}&redirect_uri=${config.redirectURI}&response_type=code&code_challenge=${challenge}&code_challenge_method=S256`);
14 | });
15 |
16 | module.exports = router;
17 |
18 |
19 |
--------------------------------------------------------------------------------
/client/app/styles/color.css:
--------------------------------------------------------------------------------
1 | /* color variables */
2 | html {
3 | --color-dark: #123;
4 | --color-light: #f6f3f0;
5 | --color-accent: #38c;
6 | }
7 |
8 | /* light on dark */
9 | header,
10 | footer,
11 | #Response pre,
12 | footer a {
13 | color: var(--color-light);
14 | background-color: var(--color-dark);
15 | }
16 |
17 | /* dark on light */
18 | header a,
19 | main,
20 | #UserData textarea:read-only {
21 | color: var(--color-dark);
22 | background-color: var(--color-light);
23 | }
24 |
25 | /* light on accent */
26 | header a:hover,
27 | header a:active {
28 | color: var(--color-light);
29 | background-color: var(--color-accent);
30 | }
31 |
32 | /* accent on dark */
33 | footer a:hover,
34 | footer a:active {
35 | color: var(--color-accent);
36 | background-color: var(--color-dark);
37 | }
38 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fusionauth-example-react-client",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "babel": {
7 | "presets": [
8 | "@babel/preset-env",
9 | "@babel/preset-react"
10 | ]
11 | },
12 | "scripts": {
13 | "start": "webpack-dev-server --open --port 4200"
14 | },
15 | "keywords": [],
16 | "author": "Matt Boisseau ",
17 | "license": "ISC",
18 | "dependencies": {
19 | "react": "^16.14.0",
20 | "react-dom": "^16.14.0"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.20.5",
24 | "@babel/preset-env": "^7.20.2",
25 | "@babel/preset-react": "^7.18.6",
26 | "babel-loader": "^9.1.0",
27 | "css-loader": "^6.7.2",
28 | "html-webpack-plugin": "^5.5.0",
29 | "style-loader": "^3.3.1",
30 | "webpack": "^5.75.0",
31 | "webpack-cli": "^5.0.1",
32 | "webpack-dev-server": "^4.11.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const session = require('express-session');
3 | const cors = require('cors');
4 | const config = require('../config');
5 |
6 | const app = express();
7 |
8 | // install the JSON middleware for parsing JSON bodies
9 | app.use(express.json());
10 |
11 | app.use(cors({
12 | origin: true,
13 | credentials: true
14 | }));
15 |
16 | // configure sessions
17 | app.use(session(
18 | {
19 | secret: '1234567890',
20 | resave: false,
21 | saveUninitialized: false,
22 | cookie: {
23 | secure: 'auto',
24 | httpOnly: true,
25 | maxAge: 3600000
26 | }
27 | })
28 | );
29 |
30 | // routes
31 | app.use('/user', require('./routes/user'));
32 | app.use('/login', require('./routes/login'));
33 | app.use('/oauth-callback', require('./routes/oauth-callback'));
34 | app.use('/logout', require('./routes/logout'));
35 | app.use('/set-user-data', require('./routes/set-user-data'));
36 |
37 | app.listen(config.serverPort, () => console.log(`FusionAuth example app listening on port ${config.serverPort}.`));
38 |
39 |
--------------------------------------------------------------------------------
/server/routes/oauth-callback.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const request = require('request');
4 | const config = require('../../config');
5 |
6 | router.get('/', (req, res) => {
7 | request(
8 | // POST request to /token endpoint
9 | {
10 | method: 'POST',
11 | uri: `http://localhost:${config.fusionAuthPort}/oauth2/token`,
12 | form: {
13 | 'client_id': config.clientID,
14 | 'client_secret': config.clientSecret,
15 | 'code': req.query.code,
16 | 'code_verifier': req.session.verifier,
17 | 'grant_type': 'authorization_code',
18 | 'redirect_uri': config.redirectURI
19 | }
20 | },
21 |
22 | // callback
23 | (error, response, body) => {
24 | if (error !== null)
25 | console.log("error:", error);
26 | else
27 | console.log("body:", body);
28 |
29 | // save token to session
30 | req.session.token = JSON.parse(body).access_token;
31 |
32 | // redirect to the Angular app
33 | res.redirect(`http://localhost:${config.clientPort}`);
34 | }
35 | );
36 | });
37 |
38 | module.exports = router;
39 |
40 |
--------------------------------------------------------------------------------
/client/app/components/UserData.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class LogInOut extends React.Component {
4 | constructor(props) {
5 | super(props);
6 | }
7 |
8 | render() {
9 | // placeholder text for the textarea
10 | let placeholder = 'Anything you type in here will be saved to your FusionAuth user data.';
11 |
12 | // the user's data.userData (or an empty string if uninitialized)
13 | let userData = (this.props.body.registration && this.props.body.registration.data)
14 | ? this.props.body.registration.data.userData
15 | : '';
16 |
17 | // textarea (locked if user not logged in)
18 | let input = (this.props.body.tid)
19 | ?
20 | : ;
21 |
22 | // section title
23 | let title = (this.props.body.tid)
24 | ? Your User Data
25 | : Sign In to Edit Your User Data
;
26 |
27 | // JSX return
28 | return (
29 |
30 | {title}
31 | {input}
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/routes/set-user-data.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const request = require('request');
4 | const config = require('../../config');
5 |
6 | router.post('/', (req, res) => {
7 | // fetch the user using the token in the session so that we have their ID
8 | request(
9 | {
10 | method: 'GET',
11 | uri: `http://localhost:${config.fusionAuthPort}/oauth2/userinfo`,
12 | headers: {
13 | 'Authorization': 'Bearer ' + req.session.token
14 | }
15 | },
16 |
17 | // callback
18 | (error, response, body) => {
19 | let userInfoResponse = JSON.parse(body);
20 | request(
21 | // PATCH request to /registration endpoint
22 | {
23 | method: 'PATCH',
24 | uri: `http://localhost:${config.fusionAuthPort}/api/user/registration/${userInfoResponse.sub}/${config.applicationID}`,
25 | headers: {
26 | 'Authorization': config.apiKey
27 | },
28 | json: true,
29 | body: {
30 | 'registration': {
31 | 'data': req.body
32 | }
33 | }
34 | },
35 | (err2, response2, body2) => {
36 | if (err2) {
37 | console.log(err2);
38 | }
39 | }
40 | );
41 | }
42 | );
43 | });
44 |
45 | module.exports = router;
46 |
47 |
--------------------------------------------------------------------------------
/server/routes/user.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const request = require('request');
4 | const config = require('../../config');
5 |
6 | router.get('/', (req, res) => {
7 | // token in session -> get user data and send it back to the Angular app
8 | if (req.session.token) {
9 | request(
10 | {
11 | method: 'GET',
12 | uri: `http://localhost:${config.fusionAuthPort}/oauth2/userinfo`,
13 | headers: {
14 | 'Authorization': 'Bearer ' + req.session.token
15 | }
16 | },
17 |
18 | // callback
19 | (error, response, body) => {
20 | let userInfoResponse = JSON.parse(body);
21 |
22 | // valid token -> get more user data and send it back to the Angular app
23 | request(
24 | // GET request to /registration endpoint
25 | {
26 | method: 'GET',
27 | uri: `http://localhost:${config.fusionAuthPort}/api/user/registration/${userInfoResponse.sub}/${config.applicationID}`,
28 | json: true,
29 | headers: {
30 | 'Authorization': config.apiKey
31 | }
32 | },
33 |
34 | // callback
35 | (error, response, body) => {
36 | res.send(
37 | {
38 | ...userInfoResponse,
39 | ...body // body is results from the registration endpoint
40 |
41 | }
42 | );
43 | }
44 | );
45 | }
46 | );
47 | }
48 |
49 | // no token -> send nothing
50 | else {
51 | res.send({});
52 | }
53 | });
54 |
55 | module.exports = router;
56 |
57 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | db:
5 | image: postgres:12.9
6 | environment:
7 | PGDATA: /var/lib/postgresql/data/pgdata
8 | POSTGRES_USER: ${POSTGRES_USER}
9 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
10 | healthcheck:
11 | test: [ "CMD-SHELL", "pg_isready -U postgres" ]
12 | interval: 5s
13 | timeout: 5s
14 | retries: 5
15 | networks:
16 | - db_net
17 | restart: unless-stopped
18 | volumes:
19 | - db_data:/var/lib/postgresql/data
20 |
21 | search:
22 | image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
23 | environment:
24 | cluster.name: fusionauth
25 | bootstrap.memory_lock: "true"
26 | discovery.type: single-node
27 | ES_JAVA_OPTS: ${ES_JAVA_OPTS}
28 | healthcheck:
29 | test: [ "CMD", "curl", "--fail" ,"--write-out", "'HTTP %{http_code}'", "--silent", "--output", "/dev/null", "http://localhost:9200/" ]
30 | interval: 5s
31 | timeout: 5s
32 | retries: 5
33 | networks:
34 | - search_net
35 | restart: unless-stopped
36 | ulimits:
37 | memlock:
38 | soft: -1
39 | hard: -1
40 | volumes:
41 | - search_data:/usr/share/elasticsearch/data
42 |
43 | fusionauth:
44 | image: fusionauth/fusionauth-app:latest
45 | depends_on:
46 | db:
47 | condition: service_healthy
48 | search:
49 | condition: service_healthy
50 | environment:
51 | DATABASE_URL: jdbc:postgresql://db:5432/fusionauth
52 | DATABASE_ROOT_USERNAME: ${POSTGRES_USER}
53 | DATABASE_ROOT_PASSWORD: ${POSTGRES_PASSWORD}
54 | DATABASE_USERNAME: ${DATABASE_USERNAME}
55 | DATABASE_PASSWORD: ${DATABASE_PASSWORD}
56 | FUSIONAUTH_APP_MEMORY: ${FUSIONAUTH_APP_MEMORY}
57 | FUSIONAUTH_APP_RUNTIME_MODE: development
58 | FUSIONAUTH_APP_URL: http://fusionauth:9011
59 | SEARCH_SERVERS: http://search:9200
60 | SEARCH_TYPE: elasticsearch
61 | FUSIONAUTH_APP_KICKSTART_FILE: ${FUSIONAUTH_APP_KICKSTART_FILE}
62 |
63 | networks:
64 | - db_net
65 | - search_net
66 | restart: unless-stopped
67 | ports:
68 | - 9011:9011
69 | volumes:
70 | - fusionauth_config:/usr/local/fusionauth/config
71 | - ./kickstart:/usr/local/fusionauth/kickstart
72 |
73 | networks:
74 | db_net:
75 | driver: bridge
76 | search_net:
77 | driver: bridge
78 |
79 | volumes:
80 | db_data:
81 | fusionauth_config:
82 | search_data:
83 |
--------------------------------------------------------------------------------
/client/app/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 |
5 | import Greeting from './components/Greeting.js';
6 | import LogInOut from './components/LogInOut.js';
7 | import Response from './components/Response.js';
8 | import UserData from './components/UserData.js';
9 |
10 | const config = require('../../config');
11 |
12 | class App extends React.Component {
13 | constructor(props) {
14 | super(props);
15 | this.state = {
16 | body: {} // this is the body from /user
17 | };
18 | this.handleTextInput = this.handleTextInput.bind(this);
19 | }
20 |
21 | componentDidMount() {
22 | fetch(`http://localhost:${config.serverPort}/user`, {
23 | credentials: 'include' // fetch won't send cookies unless you set credentials
24 | })
25 | .then(response => response.json())
26 | .then(data => { console.log("data", data); this.setState({ body: data }); });
27 | }
28 |
29 | handleTextInput(event) {
30 | // update this.state.body.registration.data.userData
31 | let body = this.state.body;
32 | if (body['registration'] === undefined)
33 | body['registration'] = {};
34 |
35 | body.registration.data = {userData: event.target.value};
36 | this.setState(
37 | {
38 | body: body
39 | });
40 |
41 | // save the change in FusionAuth
42 | fetch(`http://localhost:${config.serverPort}/set-user-data`,
43 | {
44 | credentials: 'include',
45 | method: 'POST',
46 | headers: {
47 | 'Content-Type': 'application/json'
48 | },
49 | body: JSON.stringify(
50 | {
51 | userData: event.target.value
52 | })
53 | });
54 | }
55 |
56 | render() {
57 | return (
58 |
59 |
60 | FusionAuth Example: React
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
72 |
73 | );
74 | }
75 | }
76 |
77 | ReactDOM.render(, document.querySelector('#Container'));
78 |
--------------------------------------------------------------------------------
/kickstart/kickstart.json:
--------------------------------------------------------------------------------
1 | {
2 | "variables": {
3 | "applicationId": "E9FDB985-9173-4E01-9D73-AC2D60D1DC8E",
4 | "apiKey": "this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works",
5 | "asymmetricKeyId": "#{UUID()}",
6 | "defaultTenantId": "d7d09513-a3f5-401c-9685-34ab6c552453",
7 | "adminEmail": "admin@example.com",
8 | "adminPassword": "password",
9 | "userEmail": "richard@example.com",
10 | "userPassword": "password",
11 | "userUserId": "00000000-0000-0000-0000-111111111111"
12 | },
13 | "apiKeys": [
14 | {
15 | "key": "#{apiKey}",
16 | "description": "Unrestricted API key"
17 | }
18 | ],
19 | "requests": [
20 | {
21 | "method": "POST",
22 | "url": "/api/key/generate/#{asymmetricKeyId}",
23 | "tenantId": "#{defaultTenantId}",
24 | "body": {
25 | "key": {
26 | "algorithm": "RS256",
27 | "name": "For exampleapp",
28 | "length": 2048
29 | }
30 | }
31 | },
32 | {
33 | "method": "POST",
34 | "url": "/api/user/registration",
35 | "body": {
36 | "user": {
37 | "email": "#{adminEmail}",
38 | "password": "#{adminPassword}"
39 | },
40 | "registration": {
41 | "applicationId": "#{FUSIONAUTH_APPLICATION_ID}",
42 | "roles": [
43 | "admin"
44 | ]
45 | }
46 | }
47 | },
48 | {
49 | "method": "POST",
50 | "url": "/api/application/#{applicationId}",
51 | "tenantId": "#{defaultTenantId}",
52 | "body": {
53 | "application": {
54 | "name": "exampleapp",
55 | "oauthConfiguration" : {
56 | "authorizedRedirectURLs": ["http://localhost:3000/oauth-callback"],
57 | "clientSecret": "super-secret-secret-that-should-be-regenerated-for-production",
58 | "logoutURL": "http://localhost:4200",
59 | "enabledGrants": ["authorization_code"]
60 | },
61 | "jwtConfiguration": {
62 | "enabled": true,
63 | "accessTokenKeyId": "#{asymmetricKeyId}",
64 | "idTokenKeyId": "#{asymmetricKeyId}"
65 | }
66 | }
67 | }
68 | },
69 | {
70 | "method": "POST",
71 | "url": "/api/user/registration/#{userUserId}",
72 | "body": {
73 | "user": {
74 | "birthDate": "1985-11-23",
75 | "email": "#{userEmail}",
76 | "firstName": "Richard",
77 | "lastName": "Hendricks",
78 | "password": "#{userPassword}"
79 | },
80 | "registration": {
81 | "applicationId": "#{applicationId}"
82 | }
83 | }
84 | }
85 | ]
86 | }
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **This repo is out of date and is archived. Check out [an updated tutorial on using FusionAuth with React](https://fusionauth.io/docs/quickstarts/quickstart-javascript-react-web) or [the updated GitHub repository](https://github.com/fusionauth/fusionauth-quickstart-javascript-react-web).**
2 |
3 | # Example: Using React with FusionAuth
4 |
5 | ------
6 |
7 | This project contains an example project that illustrates using FusionAuth with a React front-end and a NodeJS/Express backend. This application will use an OAuth Authorization Code workflow and the PKCE (PKCE stands for Proof Key for Code Exchange, and is often pronounced “pixie”) extension to log users in and a NodeJS backend to store your access token securely.
8 |
9 | You can read the blog post here: https://fusionauth.io/blog/2020/03/10/securely-implement-oauth-in-react/
10 |
11 | ## React Version
12 |
13 | This project runs React 16. This is an old version. Please see https://github.com/FusionAuth/fusionauth-example-react-2.0/ if you are looking for a React 17 example.
14 |
15 | ## Prerequisites
16 | You will need the following things properly installed on your computer.
17 |
18 | * [Git](http://git-scm.com/): Presumably you already have this on your machine if you are looking at this project locally; if not, use your platform's package manager to install git, and `git clone` this repo.
19 | * [NodeJS](https://nodejs.org): This will install the NodeJS runtime, which includes the package management tool `npm` needed for pulling down the various dependencies.
20 | * [Docker](https://www.docker.com): For standing up FusionAuth from within a Docker container. (You can [install it other ways](https://fusionauth.io/docs/v1/tech/installation-guide/), but for this example you'll need Docker.)
21 |
22 | ## Installation
23 | To install, do the following in a shell/Terminal window:
24 |
25 | * `git clone https://github.com/fusionauth/fusionauth-example-react` or `gh repo clone fusionauth/fusionauth-example-react`
26 | * `cd fusionauth-example-react`: This is the root of the example.
27 | * `cd client; npm install`: This will bring all the node modules onto the machine.
28 | * `cd ../server; npm install`: Likewise.
29 |
30 | ## FusionAuth Configuration
31 | This example assumes that you will run FusionAuth from a Docker container. In the root of this project directory (next to this README) are two files [a Docker compose file](./docker-compose.yml) and an [environment variables configuration file](./.env). Assuming you have Docker installed on your machine, a `docker-compose up` will bring FusionAuth up on your machine.
32 |
33 | The FusionAuth configuration files also make use of a unique feature of FusionAuth, called Kickstart: when FusionAuth comes up for the first time, it will look at the [Kickstart file](./kickstart/kickstart.json) and mimic API calls to configure FusionAuth for use. It will perform all the necessary setup to make this demo work correctly, but if you are curious as to what the setup would look like by hand, the "FusionAuth configuration (by hand)" section of this README describes it in detail.
34 |
35 | For now, get FusionAuth in Docker up and running (via `docker-compose up`) if it is not already running; to see, [click here](http://localhost:9011/) to verify it is up and running.
36 |
37 | > **NOTE**: If you ever want to reset the FusionAuth system, delete the volumes created by docker-compose by executing `docker-compose down -v`. FusionAuth will only apply the Kickstart settings when it is first run (e.g., it has no data configured for it yet).
38 |
39 | ## Running
40 | To run, do the following:
41 |
42 | * In one shell, run `docker-compose up`
43 | * In another shell, `cd server` and `npm run start`
44 | * In a third shell, `cd client` and `npm run start`
45 |
46 | [Open a browser to the React app](http://localhost:4200/). The app will automatically reload if you change any of the source files.
47 |
48 | ## Architecture
49 | The app has three parts, each running on a different `localhost` port:
50 |
51 | - `localhost:4200` is the React app. It has a single route (`/`) and makes calls to the Express app.
52 | - `localhost:3000` is the Express app. It has several routes (like `/login` and `/logout`), which are used by the React front-end. The Express app makes calls to FusionAuth.
53 | - `localhost:9011` is your instance of FusionAuth. It has several endpoints (like `/authorize` and `/introspect`). It accepts calls from the Express app and sends back information, such as access tokens and user registration data.
54 |
55 | So, the parts connect like this:
56 |
57 | `React (4200) <---> Express (3000) <---> FusionAuth (9011)`
58 |
59 | The React app never talks directly to FusionAuth. This is important, because the React app can be easily picked apart by anyone online (it's Javascript, which means the source is directly visible to anyone with a browser), which means you can't keep confidential information there. While some calls directly to FusionAuth are safe, it's usually important to keep things separated like this.
60 |
61 | ### Logging In/Out
62 |
63 | When the user clicks on `sign in`, the React app redirects to the Express server's `/login` route, which redirects to FusionAuth's `authorize` endpoint. FusionAuth renders the username/password form, authenticates the user, and redirects to the configured Redirect URI (`/oauth-redirect` on the Express server) with an Authorization Code.
64 |
65 | The Express server sends the Authorization Code (as well as its Client ID and Secret) to FusionAuth's `/token` endpoint. FusionAuth validates everything and sends back an Access Token. The Express Server saves this token in session storage and redirects back to the React client.
66 |
67 | When the user clicks on `sign out`, the React app sends a request to the Express server's `/logout` route, which sends a request to FusionAuth's `/logout` endpoint, deletes the relevant cookie, and deletes the Access Token from session storage.
68 |
69 | **The presence of the Access Token in session storage is what defines whether or not a user is logged in**, because FusionAuth will not allow retrieval or modification of user data without a valid Access Token.
70 |
71 | ### Rendering the React App
72 |
73 | When the React client mounts, it sends a request to the Express server's `/user` route. If there's an Access Token in session storage, the Express server uses FusionAuth's `/introspect` and `/registration` endpoints to get data for the current user; these give us the `token` and `registration` JSON objects seen in the example app.
74 |
75 | If there is no Access Token (or if it's expired), `/user` will instead return an empty object. The React components use the existence of `token` (or lack thereof) to determine whether to render the page in its logged-in or logged-out state.
76 |
77 | ### Editing User Data
78 |
79 | All of your FusionAuth users have a `registration.data` object for storing arbitrary data related to the user. The example app allows logged-in users to modify `registration.data.userData` by changing its value in the `