More specific access control headers to support cross origin Authorization
Pretty sign in page Verify existing JWT on application mount
This commit is contained in:
parent
04e852f7d9
commit
396d0333b6
@ -1 +1 @@
|
|||||||
REACT_APP_ENDPOINT_ROOT=http://192.168.0.4/rest/
|
REACT_APP_ENDPOINT_ROOT=http://192.168.0.16/rest/
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
REACT_APP_ENDPOINT_ROOT=/rest/
|
REACT_APP_ENDPOINT_ROOT=/rest/
|
||||||
GENERATE_SOURCEMAP=false
|
GENERATE_SOURCEMAP=false
|
||||||
|
@ -29,11 +29,13 @@ export function fetchLoginRedirect() {
|
|||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene with one with provides the access token if present.
|
* Wraps the normal fetch routene with one with provides the access token if present.
|
||||||
*/
|
*/
|
||||||
export function secureFetch(url, params) {
|
export function authorizedFetch(url, params) {
|
||||||
if (localStorage.getItem(ACCESS_TOKEN)) {
|
const accessToken = localStorage.getItem(ACCESS_TOKEN);
|
||||||
|
if (accessToken) {
|
||||||
params = params || {};
|
params = params || {};
|
||||||
params.headers = params.headers || new Headers();
|
params.credentials = 'include';
|
||||||
params.headers.Authorization = 'Bearer ' + localStorage.getItem(ACCESS_TOKEN)
|
params.headers = params.headers || {};
|
||||||
|
params.headers.Authorization = 'Bearer ' + accessToken;
|
||||||
}
|
}
|
||||||
return fetch(url, params);
|
return fetch(url, params);
|
||||||
}
|
}
|
||||||
@ -41,9 +43,9 @@ export function secureFetch(url, params) {
|
|||||||
/**
|
/**
|
||||||
* Wraps the normal fetch routene which redirects on 401 response.
|
* 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) {
|
return new Promise(function (resolve, reject) {
|
||||||
secureFetch(url, params).then(response => {
|
authorizedFetch(url, params).then(response => {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
history.go("/");
|
history.go("/");
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,10 +1,27 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { withNotifier } from '../components/SnackbarNotification';
|
import { withNotifier } from '../components/SnackbarNotification';
|
||||||
|
import { VERIFY_AUTHORIZATION_ENDPOINT } from '../constants/Endpoints';
|
||||||
import { ACCESS_TOKEN } from './Authentication';
|
import { ACCESS_TOKEN, authorizedFetch } from './Authentication';
|
||||||
import { AuthenticationContext } from './Context';
|
import { AuthenticationContext } from './Context';
|
||||||
import jwtDecode from 'jwt-decode';
|
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 {
|
class AuthenticationWrapper extends React.Component {
|
||||||
|
|
||||||
@ -44,23 +61,30 @@ class AuthenticationWrapper extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderContentLoading() {
|
renderContentLoading() {
|
||||||
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>THIS IS WHERE THE LOADING MESSAGE GOES</div>
|
<div className={classes.loadingPanel}>
|
||||||
|
<CircularProgress className={classes.progress} size={100} />
|
||||||
|
<Typography variant="h4" >
|
||||||
|
Loading...
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
var accessToken = localStorage.getItem(ACCESS_TOKEN);
|
var accessToken = localStorage.getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
try {
|
authorizedFetch(VERIFY_AUTHORIZATION_ENDPOINT)
|
||||||
this.setState({ initialized: true, context: { ...this.state.context, jwt: jwtDecode(accessToken) } });
|
.then(response => {
|
||||||
} catch (err) {
|
const jwt = response.status === 200 ? jwtDecode(accessToken) : undefined;
|
||||||
localStorage.removeItem(ACCESS_TOKEN);
|
this.setState({ initialized: true, context: { ...this.state.context, jwt } });
|
||||||
this.props.raiseNotification("Please log in again.");
|
}).catch(error => {
|
||||||
history.push('/');
|
this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } });
|
||||||
}
|
this.props.raiseNotification("Error verifying authorization: " + error.message);
|
||||||
|
});
|
||||||
} else {
|
} 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) } });
|
this.setState({ context: { ...this.state.context, jwt: jwtDecode(accessToken) } });
|
||||||
localStorage.setItem(ACCESS_TOKEN, accessToken);
|
localStorage.setItem(ACCESS_TOKEN, accessToken);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.props.raiseNotification("JWT did not parse.");
|
this.setState({ initialized: true, context: { ...this.state.context, jwt: undefined } });
|
||||||
history.push('/');
|
this.props.raiseNotification("Failed to parse JWT " + err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +103,7 @@ class AuthenticationWrapper extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
context: {
|
context: {
|
||||||
...this.state.context,
|
...this.state.context,
|
||||||
me: undefined
|
jwt: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.props.raiseNotification("You have signed out.");
|
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))
|
||||||
|
@ -54,7 +54,7 @@ class SnackbarNotification extends React.Component {
|
|||||||
open={this.state.open}
|
open={this.state.open}
|
||||||
autoHideDuration={6000}
|
autoHideDuration={6000}
|
||||||
onClose={this.handleClose}
|
onClose={this.handleClose}
|
||||||
SnackbarContentProps={{
|
ContentProps={{
|
||||||
'aria-describedby': 'message-id',
|
'aria-describedby': 'message-id',
|
||||||
}}
|
}}
|
||||||
message={<span id="message-id">{this.state.message}</span>}
|
message={<span id="message-id">{this.state.message}</span>}
|
||||||
|
@ -9,4 +9,5 @@ export const LIST_NETWORKS_ENDPOINT = ENDPOINT_ROOT + "listNetworks";
|
|||||||
export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "wifiSettings";
|
export const WIFI_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "wifiSettings";
|
||||||
export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + "wifiStatus";
|
export const WIFI_STATUS_ENDPOINT = ENDPOINT_ROOT + "wifiStatus";
|
||||||
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
|
export const OTA_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "otaSettings";
|
||||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||||
|
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
import { withStyles } from '@material-ui/core/styles';
|
import { withStyles } from '@material-ui/core/styles';
|
||||||
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
import { TextValidator, ValidatorForm } from 'react-material-ui-form-validator';
|
||||||
import Paper from '@material-ui/core/Paper';
|
import Paper from '@material-ui/core/Paper';
|
||||||
@ -10,41 +9,42 @@ import ForwardIcon from '@material-ui/icons/Forward';
|
|||||||
import { withNotifier } from '../components/SnackbarNotification';
|
import { withNotifier } from '../components/SnackbarNotification';
|
||||||
import { SIGN_IN_ENDPOINT } from '../constants/Endpoints';
|
import { SIGN_IN_ENDPOINT } from '../constants/Endpoints';
|
||||||
import { withAuthenticationContext } from '../authentication/Context';
|
import { withAuthenticationContext } from '../authentication/Context';
|
||||||
|
import PasswordValidator from '../components/PasswordValidator';
|
||||||
|
|
||||||
const styles = theme => ({
|
const styles = theme => {
|
||||||
loginPage: {
|
return {
|
||||||
padding: theme.spacing.unit * 2,
|
loginPage: {
|
||||||
height: "100vh",
|
display: "flex",
|
||||||
display: "flex"
|
height: "100vh",
|
||||||
},
|
margin: "auto",
|
||||||
loginPanel: {
|
padding: theme.spacing.unit * 2,
|
||||||
margin: "auto",
|
justifyContent: "center",
|
||||||
padding: theme.spacing.unit * 2,
|
flexDirection: "column",
|
||||||
paddingTop: "200px",
|
maxWidth: theme.breakpoints.values.sm
|
||||||
backgroundImage: 'url("/app/icon.png")',
|
},
|
||||||
backgroundRepeat: "no-repeat",
|
loginPanel: {
|
||||||
backgroundPosition: "50% " + theme.spacing.unit * 2 + "px",
|
textAlign: "center",
|
||||||
backgroundSize: "auto 150px",
|
padding: theme.spacing.unit * 2,
|
||||||
textAlign: "center"
|
paddingTop: "200px",
|
||||||
},
|
backgroundImage: 'url("/app/icon.png")',
|
||||||
extendedIcon: {
|
backgroundRepeat: "no-repeat",
|
||||||
marginRight: theme.spacing.unit,
|
backgroundPosition: "50% " + theme.spacing.unit * 2 + "px",
|
||||||
},
|
backgroundSize: "auto 150px",
|
||||||
loadingSettings: {
|
width: "100%"
|
||||||
margin: theme.spacing.unit,
|
},
|
||||||
},
|
extendedIcon: {
|
||||||
loadingSettingsDetails: {
|
marginRight: theme.spacing.unit,
|
||||||
margin: theme.spacing.unit * 4,
|
},
|
||||||
textAlign: "center"
|
textField: {
|
||||||
},
|
width: "100%"
|
||||||
textField: {
|
},
|
||||||
width: "100%"
|
button: {
|
||||||
},
|
marginRight: theme.spacing.unit * 2,
|
||||||
button: {
|
marginTop: theme.spacing.unit * 2,
|
||||||
marginRight: theme.spacing.unit * 2,
|
}
|
||||||
marginTop: theme.spacing.unit * 2,
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
class LoginPage extends Component {
|
class LoginPage extends Component {
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class LoginPage extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
fetched: false
|
processing: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ class LoginPage extends Component {
|
|||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
const { username, password } = this.state;
|
const { username, password } = this.state;
|
||||||
const { authenticationContext } = this.props;
|
const { authenticationContext } = this.props;
|
||||||
this.setState({ fetched: false });
|
this.setState({ processing: true });
|
||||||
fetch(SIGN_IN_ENDPOINT, {
|
fetch(SIGN_IN_ENDPOINT, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ username, password }),
|
||||||
@ -76,21 +76,22 @@ class LoginPage extends Component {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return response.json();
|
return response.json();
|
||||||
} else if (response.status === 401) {
|
} else if (response.status === 401) {
|
||||||
throw Error("Login details invalid!");
|
throw Error("Invalid login details.");
|
||||||
} else {
|
} else {
|
||||||
throw Error("Invalid status code: " + response.status);
|
throw Error("Invalid status code: " + response.status);
|
||||||
}
|
}
|
||||||
}).then(json =>{
|
}).then(json => {
|
||||||
authenticationContext.signIn(json.access_token);
|
authenticationContext.signIn(json.access_token);
|
||||||
|
this.setState({ processing: false });
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.props.raiseNotification(error.message);
|
this.props.raiseNotification(error.message);
|
||||||
this.setState({ fetched: true });
|
this.setState({ processing: false });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { username, password } = this.state;
|
const { username, password, processing } = this.state;
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classes.loginPage}>
|
<div className={classes.loginPage}>
|
||||||
@ -98,6 +99,7 @@ class LoginPage extends Component {
|
|||||||
<Typography variant="h4">{APP_NAME}</Typography>
|
<Typography variant="h4">{APP_NAME}</Typography>
|
||||||
<ValidatorForm onSubmit={this.onSubmit}>
|
<ValidatorForm onSubmit={this.onSubmit}>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
|
disabled={processing}
|
||||||
validators={['required']}
|
validators={['required']}
|
||||||
errorMessages={['Username is required']}
|
errorMessages={['Username is required']}
|
||||||
name="username"
|
name="username"
|
||||||
@ -107,7 +109,8 @@ class LoginPage extends Component {
|
|||||||
onChange={this.handleValueChange('username')}
|
onChange={this.handleValueChange('username')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<TextValidator
|
<PasswordValidator
|
||||||
|
disabled={processing}
|
||||||
validators={['required']}
|
validators={['required']}
|
||||||
errorMessages={['Password is required']}
|
errorMessages={['Password is required']}
|
||||||
name="password"
|
name="password"
|
||||||
@ -117,7 +120,7 @@ class LoginPage extends Component {
|
|||||||
onChange={this.handleValueChange('password')}
|
onChange={this.handleValueChange('password')}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
<Fab variant="extended" color="primary" className={classes.button} type="submit">
|
<Fab variant="extended" color="primary" className={classes.button} type="submit" disabled={processing}>
|
||||||
<ForwardIcon className={classes.extendedIcon} />
|
<ForwardIcon className={classes.extendedIcon} />
|
||||||
Sign In
|
Sign In
|
||||||
</Fab>
|
</Fab>
|
||||||
|
@ -19,7 +19,8 @@ monitor_speed = 115200
|
|||||||
|
|
||||||
build_flags=
|
build_flags=
|
||||||
-D NO_GLOBAL_ARDUINOOTA
|
-D NO_GLOBAL_ARDUINOOTA
|
||||||
; -D ENABLE_CORS
|
-D ENABLE_CORS
|
||||||
|
-D CORS_ORIGIN=\"http://localhost:3000\"
|
||||||
lib_deps =
|
lib_deps =
|
||||||
NtpClientLib@>=2.5.1,<3.0.0
|
NtpClientLib@>=2.5.1,<3.0.0
|
||||||
ArduinoJson@>=6.0.0,<7.0.0
|
ArduinoJson@>=6.0.0,<7.0.0
|
||||||
|
@ -77,8 +77,9 @@ void setup() {
|
|||||||
|
|
||||||
// Disable CORS if required
|
// Disable CORS if required
|
||||||
#if defined(ENABLE_CORS)
|
#if defined(ENABLE_CORS)
|
||||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", CORS_ORIGIN);
|
||||||
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization");
|
||||||
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Credentials", "true");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
server.begin();
|
server.begin();
|
||||||
|
Loading…
Reference in New Issue
Block a user