feat: add generate token endpoint and ui for generating tokens for users

This commit is contained in:
proddy
2021-04-21 21:16:38 +02:00
parent 8ea48f7c81
commit a951ebc3ed
11 changed files with 1720 additions and 6 deletions

View File

@@ -18,5 +18,6 @@ export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";

View File

@@ -0,0 +1,77 @@
import React, { Fragment } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextField } from '@material-ui/core';
import { FormButton } from '../components';
import { redirectingAuthorizedFetch } from '../authentication';
import { GENERATE_TOKEN_ENDPOINT } from '../api';
import { withSnackbar, WithSnackbarProps } from 'notistack';
interface GenerateTokenProps extends WithSnackbarProps {
username: string;
onClose: () => void;
}
interface GenerateTokenState {
token?: string;
}
class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenState> {
state: GenerateTokenState = {};
componentDidMount() {
const { username } = this.props;
redirectingAuthorizedFetch(GENERATE_TOKEN_ENDPOINT + "?" + new URLSearchParams({ username }), { method: 'GET' })
.then(response => {
if (response.status === 200) {
return response.json();
} else {
throw Error("Error generating token: " + response.status);
}
}).then(generatedToken => {
console.log(generatedToken);
this.setState({ token: generatedToken.token });
})
.catch(error => {
this.props.enqueueSnackbar(error.message || "Problem generating token", { variant: 'error' });
});
}
render() {
const { onClose, username } = this.props;
const { token } = this.state;
return (
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="generate-token-dialog-title">Token for: {username}</DialogTitle>
<DialogContent dividers>
{token ?
<Fragment>
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
<Typography variant="body1">
The token below may be used to access the secured APIs. This may be used for bearer authentication with the "Authorization" header or using the "access_token" query paramater.
</Typography>
</Box>
<Box mt={2} mb={2}>
<TextField label="Token" multiline value={token} fullWidth contentEditable={false} />
</Box>
</Fragment>
:
<Box m={4} textAlign="center">
<LinearProgress />
<Typography variant="h6">
Generating token&hellip;
</Typography>
</Box>
}
</DialogContent>
<DialogActions>
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
Close
</FormButton>
</DialogActions>
</Dialog>
);
}
}
export default withSnackbar(GenerateToken);

View File

@@ -11,12 +11,14 @@ import CheckIcon from '@material-ui/icons/Check';
import IconButton from '@material-ui/core/IconButton';
import SaveIcon from '@material-ui/icons/Save';
import PersonAddIcon from '@material-ui/icons/PersonAdd';
import VpnKeyIcon from '@material-ui/icons/VpnKey';
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
import UserForm from './UserForm';
import { SecuritySettings, User } from './types';
import GenerateToken from './GenerateToken';
function compareUsers(a: User, b: User) {
if (a.username < b.username) {
@@ -33,6 +35,7 @@ type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedConte
type ManageUsersFormState = {
creating: boolean;
user?: User;
generateTokenFor?: string;
}
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
@@ -66,6 +69,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
this.props.setData({ ...data, users });
}
closeGenerateToken = () => {
this.setState({
generateTokenFor: undefined
});
}
generateToken = (user: User) => {
this.setState({
generateTokenFor: user.username
});
}
startEditingUser = (user: User) => {
this.setState({
creating: false,
@@ -103,7 +118,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
render() {
const { width, data } = this.props;
const { user, creating } = this.state;
const { user, creating, generateTokenFor } = this.state;
return (
<Fragment>
<ValidatorForm onSubmit={this.onSubmit}>
@@ -125,6 +140,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
{user.admin ? <CheckIcon /> : <CloseIcon />}
</TableCell>
<TableCell align="center">
<IconButton size="small" aria-label="Generate Token" onClick={() => this.generateToken(user)}>
<VpnKeyIcon />
</IconButton>
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
<DeleteIcon />
</IconButton>
@@ -162,6 +180,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
</FormButton>
</FormActions>
</ValidatorForm>
{
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
}
{
user &&
<UserForm

View File

@@ -32,7 +32,7 @@ class UserForm extends React.Component<UserFormProps> {
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
return (
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open>
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open fullWidth maxWidth="sm">
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
<DialogContent dividers>
<TextValidator

View File

@@ -9,3 +9,6 @@ export interface SecuritySettings {
jwt_secret: string;
}
export interface GeneratedToken {
token: string;
}

View File

@@ -9,6 +9,7 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer * server, FS * f
, _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE)
, _jwtHandler(FACTORY_JWT_SECRET) {
addUpdateHandler([&](const String & originId) { configureJWTHandler(); }, false);
server->on(GENERATE_TOKEN_PATH, HTTP_GET, wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
}
void SecuritySettingsService::begin() {
@@ -109,6 +110,21 @@ ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequest
};
}
void SecuritySettingsService::generateToken(AsyncWebServerRequest* request) {
AsyncWebParameter* usernameParam = request->getParam("username");
for (User _user : _state.users) {
if (_user.username == usernameParam->value()) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, GENERATE_TOKEN_SIZE);
JsonObject root = response->getRoot();
root["token"] = generateJWT(&_user);
response->setLength();
request->send(response);
return;
}
}
request->send(401);
}
#else
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);

View File

@@ -25,6 +25,9 @@
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
#define GENERATE_TOKEN_SIZE 512
#define GENERATE_TOKEN_PATH "/rest/generateToken"
#if FT_ENABLED(FT_SECURITY)
class SecuritySettings {
@@ -83,6 +86,8 @@ class SecuritySettingsService : public StatefulService<SecuritySettings>, public
FSPersistence<SecuritySettings> _fsPersistence;
ArduinoJsonJWT _jwtHandler;
void generateToken(AsyncWebServerRequest * request);
void configureJWTHandler();
/*

File diff suppressed because it is too large Load Diff

View File

@@ -87,6 +87,7 @@ const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
const system_status = {
"esp_platform": "ESP32", "max_alloc_heap": 113792, "psram_size": 0, "free_psram": 0, "cpu_freq_mhz": 240,
"free_heap": 193340, "sdk_version": "v3.3.5-1-g85c43024c", "flash_chip_size": 4194304, "flash_chip_speed": 40000000,
@@ -102,6 +103,7 @@ const verify_authentication = { access_token: '1234' };
const signin = {
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInZlcnNpb24iOiIzLjAuMmIwIn0.MsHSgoJKI1lyYz77EiT5ZN3ECMrb4mPv9FNy3udq0TU"
};
const generate_token = { token: '1234' };
// EMS-ESP Project specific
const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
@@ -209,7 +211,6 @@ const emsesp_devicedata_2 = {
]
};
// NETWORK
app.get(NETWORK_STATUS_ENDPOINT, (req, res) => { res.json(network_status); });
app.get(NETWORK_SETTINGS_ENDPOINT, (req, res) => { res.json(network_settings); });
@@ -247,6 +248,7 @@ app.post(RESTART_ENDPOINT, (req, res) => { res.sendStatus(200); });
app.post(FACTORY_RESET_ENDPOINT, (req, res) => { res.sendStatus(200); });
app.post(UPLOAD_FIRMWARE_ENDPOINT, (req, res) => { res.sendStatus(200); });
app.post(SIGN_IN_ENDPOINT, (req, res) => { res.json(signin); });
app.get(GENERATE_TOKEN_ENDPOINT, (req, res) => { res.json(generate_token); });
// EMS-ESP Project stuff
app.get(EMSESP_SETTINGS_ENDPOINT, (req, res) => { res.json(emsesp_settings); });

View File

@@ -1,2 +1,2 @@
#define EMSESP_APP_VERSION "3.0.3b2"
#define EMSESP_APP_VERSION "3.0.3b3"
#define EMSESP_PLATFORM "ESP32"