From 396d0333b6f63e9a14cca06ec19492270bca4108 Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Sun, 19 May 2019 17:51:57 +0100 Subject: [PATCH] More specific access control headers to support cross origin Authorization Pretty sign in page Verify existing JWT on application mount --- interface/.env.development | 2 +- interface/.env.production | 2 +- .../src/authentication/Authentication.js | 14 +-- .../authentication/AuthenticationWrapper.js | 54 ++++++++---- .../src/components/SnackbarNotification.js | 2 +- interface/src/constants/Endpoints.js | 3 +- interface/src/containers/SignInPage.js | 87 ++++++++++--------- platformio.ini | 3 +- src/main.cpp | 5 +- 9 files changed, 102 insertions(+), 70 deletions(-) diff --git a/interface/.env.development b/interface/.env.development index fc000e6..b7c5d93 100644 --- a/interface/.env.development +++ b/interface/.env.development @@ -1 +1 @@ -REACT_APP_ENDPOINT_ROOT=http://192.168.0.4/rest/ \ No newline at end of file +REACT_APP_ENDPOINT_ROOT=http://192.168.0.16/rest/ diff --git a/interface/.env.production b/interface/.env.production index e2075f0..5f7447a 100644 --- a/interface/.env.production +++ b/interface/.env.production @@ -1,2 +1,2 @@ REACT_APP_ENDPOINT_ROOT=/rest/ -GENERATE_SOURCEMAP=false \ No newline at end of file +GENERATE_SOURCEMAP=false diff --git a/interface/src/authentication/Authentication.js b/interface/src/authentication/Authentication.js index 48fefe8..8606d4a 100644 --- a/interface/src/authentication/Authentication.js +++ b/interface/src/authentication/Authentication.js @@ -29,11 +29,13 @@ export function fetchLoginRedirect() { /** * Wraps the normal fetch routene with one with provides the access token if present. */ -export function secureFetch(url, params) { - if (localStorage.getItem(ACCESS_TOKEN)) { +export function authorizedFetch(url, params) { + const accessToken = localStorage.getItem(ACCESS_TOKEN); + if (accessToken) { params = params || {}; - params.headers = params.headers || new Headers(); - params.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN) + params.credentials = 'include'; + params.headers = params.headers || {}; + params.headers.Authorization = 'Bearer ' + accessToken; } return fetch(url, params); } @@ -41,9 +43,9 @@ export function secureFetch(url, params) { /** * Wraps the normal fetch routene which redirects on 401 response. */ -export function redirectingSecureFetch(url, params) { +export function redirectingAuthorizedFetch(url, params) { return new Promise(function (resolve, reject) { - secureFetch(url, params).then(response => { + authorizedFetch(url, params).then(response => { if (response.status === 401) { history.go("/"); } else { diff --git a/interface/src/authentication/AuthenticationWrapper.js b/interface/src/authentication/AuthenticationWrapper.js index c9cb02a..81b3fa0 100644 --- a/interface/src/authentication/AuthenticationWrapper.js +++ b/interface/src/authentication/AuthenticationWrapper.js @@ -1,10 +1,27 @@ import * as React from 'react'; import history from '../history' import { withNotifier } from '../components/SnackbarNotification'; - -import { ACCESS_TOKEN } from './Authentication'; +import { VERIFY_AUTHORIZATION_ENDPOINT } from '../constants/Endpoints'; +import { ACCESS_TOKEN, authorizedFetch } from './Authentication'; import { AuthenticationContext } from './Context'; import jwtDecode from 'jwt-decode'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Typography from '@material-ui/core/Typography'; +import { withStyles } from '@material-ui/core/styles'; + +const styles = theme => ({ + loadingPanel: { + padding: theme.spacing.unit * 2, + display: "flex", + alignItems: "center", + justifyContent: "center", + height: "100vh", + flexDirection: "column" + }, + progress: { + margin: theme.spacing.unit * 4, + } +}); class AuthenticationWrapper extends React.Component { @@ -44,23 +61,30 @@ class AuthenticationWrapper extends React.Component { } renderContentLoading() { + const { classes } = this.props; return ( -
THIS IS WHERE THE LOADING MESSAGE GOES
+
+ + + Loading... + +
); } refresh() { var accessToken = localStorage.getItem(ACCESS_TOKEN); if (accessToken) { - try { - this.setState({ initialized: true, context: { ...this.state.context, jwt: jwtDecode(accessToken) } }); - } catch (err) { - localStorage.removeItem(ACCESS_TOKEN); - this.props.raiseNotification("Please log in again."); - history.push('/'); - } + authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT) + .then(response => { + const jwt = response.status === 200 ? jwtDecode(accessToken) : undefined; + this.setState({ initialized: true, context: { ...this.state.context, jwt } }); + }).catch(error => { + this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } }); + this.props.raiseNotification("Error verifying authorization: " + error.message); + }); } else { - this.setState({ initialized: true }); + this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } }); } } @@ -69,8 +93,8 @@ class AuthenticationWrapper extends React.Component { this.setState({ context: { ...this.state.context, jwt: jwtDecode(accessToken) } }); localStorage.setItem(ACCESS_TOKEN, accessToken); } catch (err) { - this.props.raiseNotification("JWT did not parse."); - history.push('/'); + this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } }); + this.props.raiseNotification("Failed to parse JWT " + err.message); } } @@ -79,7 +103,7 @@ class AuthenticationWrapper extends React.Component { this.setState({ context: { ...this.state.context, - me: undefined + jwt: undefined } }); this.props.raiseNotification("You have signed out."); @@ -88,4 +112,4 @@ class AuthenticationWrapper extends React.Component { } -export default withNotifier(AuthenticationWrapper) +export default withStyles(styles)(withNotifier(AuthenticationWrapper)) diff --git a/interface/src/components/SnackbarNotification.js b/interface/src/components/SnackbarNotification.js index f30c434..c866c0e 100644 --- a/interface/src/components/SnackbarNotification.js +++ b/interface/src/components/SnackbarNotification.js @@ -54,7 +54,7 @@ class SnackbarNotification extends React.Component { open={this.state.open} autoHideDuration={6000} onClose={this.handleClose} - SnackbarContentProps={{ + ContentProps={{ 'aria-describedby': 'message-id', }} message={{this.state.message}} diff --git a/interface/src/constants/Endpoints.js b/interface/src/constants/Endpoints.js index 6295f29..cc13481 100644 --- a/interface/src/constants/Endpoints.js +++ b/interface/src/constants/Endpoints.js @@ -9,4 +9,5 @@ export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks"; export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "wifiSettings"; export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + "wifiStatus"; export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings"; -export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn"; \ No newline at end of file +export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn"; +export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization"; diff --git a/interface/src/containers/SignInPage.js b/interface/src/containers/SignInPage.js index 6d98ea9..f03da23 100644 --- a/interface/src/containers/SignInPage.js +++ b/interface/src/containers/SignInPage.js @@ -1,5 +1,4 @@ import React, { Component } from 'react'; - import { withStyles } from '@material-ui/core/styles'; import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator'; import Paper from '@material-ui/core/Paper'; @@ -10,41 +9,42 @@ import ForwardIcon from '@material-ui/icons/Forward'; import { withNotifier } from '../components/SnackbarNotification'; import { SIGN_IN_ENDPOINT } from '../constants/Endpoints'; import { withAuthenticationContext } from '../authentication/Context'; +import PasswordValidator from '../components/PasswordValidator'; -const styles = theme => ({ - loginPage: { - padding: theme.spacing.unit * 2, - height: "100vh", - display: "flex" - }, - loginPanel: { - margin: "auto", - padding: theme.spacing.unit * 2, - paddingTop: "200px", - backgroundImage: 'url("/app/icon.png")', - backgroundRepeat: "no-repeat", - backgroundPosition: "50% " + theme.spacing.unit * 2 + "px", - backgroundSize: "auto 150px", - textAlign: "center" - }, - extendedIcon: { - marginRight: theme.spacing.unit, - }, - loadingSettings: { - margin: theme.spacing.unit, - }, - loadingSettingsDetails: { - margin: theme.spacing.unit * 4, - textAlign: "center" - }, - textField: { - width: "100%" - }, - button: { - marginRight: theme.spacing.unit * 2, - marginTop: theme.spacing.unit * 2, +const styles = theme => { + return { + loginPage: { + display: "flex", + height: "100vh", + margin: "auto", + padding: theme.spacing.unit * 2, + justifyContent: "center", + flexDirection: "column", + maxWidth: theme.breakpoints.values.sm + }, + loginPanel: { + textAlign: "center", + padding: theme.spacing.unit * 2, + paddingTop: "200px", + backgroundImage: 'url("/app/icon.png")', + backgroundRepeat: "no-repeat", + backgroundPosition: "50% " + theme.spacing.unit * 2 + "px", + backgroundSize: "auto 150px", + width: "100%" + }, + extendedIcon: { + marginRight: theme.spacing.unit, + }, + textField: { + width: "100%" + }, + button: { + marginRight: theme.spacing.unit * 2, + marginTop: theme.spacing.unit * 2, + } } -}); +} + class LoginPage extends Component { @@ -53,7 +53,7 @@ class LoginPage extends Component { this.state = { username: '', password: '', - fetched: false + processing: false }; } @@ -64,7 +64,7 @@ class LoginPage extends Component { onSubmit = () => { const { username, password } = this.state; const { authenticationContext } = this.props; - this.setState({ fetched: false }); + this.setState({ processing: true }); fetch(SIGN_IN_ENDPOINT, { method: 'POST', body: JSON.stringify({ username, password }), @@ -76,21 +76,22 @@ class LoginPage extends Component { if (response.status === 200) { return response.json(); } else if (response.status === 401) { - throw Error("Login details invalid!"); + throw Error("Invalid login details."); } else { throw Error("Invalid status code: " + response.status); } - }).then(json =>{ + }).then(json => { authenticationContext.signIn(json.access_token); + this.setState({ processing: false }); }) .catch(error => { this.props.raiseNotification(error.message); - this.setState({ fetched: true }); + this.setState({ processing: false }); }); }; render() { - const { username, password } = this.state; + const { username, password, processing } = this.state; const { classes } = this.props; return (
@@ -98,6 +99,7 @@ class LoginPage extends Component { {APP_NAME} - - + Sign In diff --git a/platformio.ini b/platformio.ini index ea7848c..7a152e2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,7 +19,8 @@ monitor_speed = 115200 build_flags= -D NO_GLOBAL_ARDUINOOTA -; -D ENABLE_CORS + -D ENABLE_CORS + -D CORS_ORIGIN=\"http://localhost:3000\" lib_deps = NtpClientLib@>=2.5.1,<3.0.0 ArduinoJson@>=6.0.0,<7.0.0 diff --git a/src/main.cpp b/src/main.cpp index 7c9ec3c..ea0a4c5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,8 +77,9 @@ void setup() { // Disable CORS if required #if defined(ENABLE_CORS) - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); - DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true"); #endif server.begin();