mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
feat: add generate token endpoint and ui for generating tokens for users
This commit is contained in:
@@ -18,5 +18,6 @@ export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
|
|||||||
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||||
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
|
||||||
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
|
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 RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
||||||
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
||||||
|
|||||||
77
interface/src/security/GenerateToken.tsx
Normal file
77
interface/src/security/GenerateToken.tsx
Normal 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…
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</FormButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withSnackbar(GenerateToken);
|
||||||
@@ -11,12 +11,14 @@ import CheckIcon from '@material-ui/icons/Check';
|
|||||||
import IconButton from '@material-ui/core/IconButton';
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
import SaveIcon from '@material-ui/icons/Save';
|
import SaveIcon from '@material-ui/icons/Save';
|
||||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||||
|
import VpnKeyIcon from '@material-ui/icons/VpnKey';
|
||||||
|
|
||||||
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
|
||||||
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
|
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
|
||||||
|
|
||||||
import UserForm from './UserForm';
|
import UserForm from './UserForm';
|
||||||
import { SecuritySettings, User } from './types';
|
import { SecuritySettings, User } from './types';
|
||||||
|
import GenerateToken from './GenerateToken';
|
||||||
|
|
||||||
function compareUsers(a: User, b: User) {
|
function compareUsers(a: User, b: User) {
|
||||||
if (a.username < b.username) {
|
if (a.username < b.username) {
|
||||||
@@ -33,6 +35,7 @@ type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedConte
|
|||||||
type ManageUsersFormState = {
|
type ManageUsersFormState = {
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
generateTokenFor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
|
||||||
@@ -66,6 +69,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
this.props.setData({ ...data, users });
|
this.props.setData({ ...data, users });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
closeGenerateToken = () => {
|
||||||
|
this.setState({
|
||||||
|
generateTokenFor: undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generateToken = (user: User) => {
|
||||||
|
this.setState({
|
||||||
|
generateTokenFor: user.username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
startEditingUser = (user: User) => {
|
startEditingUser = (user: User) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
creating: false,
|
creating: false,
|
||||||
@@ -103,7 +118,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { width, data } = this.props;
|
const { width, data } = this.props;
|
||||||
const { user, creating } = this.state;
|
const { user, creating, generateTokenFor } = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<ValidatorForm onSubmit={this.onSubmit}>
|
<ValidatorForm onSubmit={this.onSubmit}>
|
||||||
@@ -125,6 +140,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
{user.admin ? <CheckIcon /> : <CloseIcon />}
|
{user.admin ? <CheckIcon /> : <CloseIcon />}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center">
|
<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)}>
|
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -162,6 +180,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
|
|||||||
</FormButton>
|
</FormButton>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</ValidatorForm>
|
</ValidatorForm>
|
||||||
|
{
|
||||||
|
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
|
||||||
|
}
|
||||||
{
|
{
|
||||||
user &&
|
user &&
|
||||||
<UserForm
|
<UserForm
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class UserForm extends React.Component<UserFormProps> {
|
|||||||
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
|
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
|
||||||
return (
|
return (
|
||||||
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
|
<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>
|
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<TextValidator
|
<TextValidator
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ export interface SecuritySettings {
|
|||||||
jwt_secret: string;
|
jwt_secret: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GeneratedToken {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer * server, FS * f
|
|||||||
, _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE)
|
, _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE)
|
||||||
, _jwtHandler(FACTORY_JWT_SECRET) {
|
, _jwtHandler(FACTORY_JWT_SECRET) {
|
||||||
addUpdateHandler([&](const String & originId) { configureJWTHandler(); }, false);
|
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() {
|
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
|
#else
|
||||||
|
|
||||||
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);
|
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
|
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
|
||||||
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
|
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
|
||||||
|
|
||||||
|
#define GENERATE_TOKEN_SIZE 512
|
||||||
|
#define GENERATE_TOKEN_PATH "/rest/generateToken"
|
||||||
|
|
||||||
#if FT_ENABLED(FT_SECURITY)
|
#if FT_ENABLED(FT_SECURITY)
|
||||||
|
|
||||||
class SecuritySettings {
|
class SecuritySettings {
|
||||||
@@ -83,6 +86,8 @@ class SecuritySettingsService : public StatefulService<SecuritySettings>, public
|
|||||||
FSPersistence<SecuritySettings> _fsPersistence;
|
FSPersistence<SecuritySettings> _fsPersistence;
|
||||||
ArduinoJsonJWT _jwtHandler;
|
ArduinoJsonJWT _jwtHandler;
|
||||||
|
|
||||||
|
void generateToken(AsyncWebServerRequest * request);
|
||||||
|
|
||||||
void configureJWTHandler();
|
void configureJWTHandler();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
1591
mock-api/package-lock.json
generated
1591
mock-api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -87,6 +87,7 @@ const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
|
|||||||
const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";
|
||||||
const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
const UPLOAD_FIRMWARE_ENDPOINT = ENDPOINT_ROOT + "uploadFirmware";
|
||||||
const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
|
||||||
|
const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
|
||||||
const system_status = {
|
const system_status = {
|
||||||
"esp_platform": "ESP32", "max_alloc_heap": 113792, "psram_size": 0, "free_psram": 0, "cpu_freq_mhz": 240,
|
"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,
|
"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 = {
|
const signin = {
|
||||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInZlcnNpb24iOiIzLjAuMmIwIn0.MsHSgoJKI1lyYz77EiT5ZN3ECMrb4mPv9FNy3udq0TU"
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInZlcnNpb24iOiIzLjAuMmIwIn0.MsHSgoJKI1lyYz77EiT5ZN3ECMrb4mPv9FNy3udq0TU"
|
||||||
};
|
};
|
||||||
|
const generate_token = { token: '1234' };
|
||||||
|
|
||||||
// EMS-ESP Project specific
|
// EMS-ESP Project specific
|
||||||
const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
const EMSESP_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "emsespSettings";
|
||||||
@@ -209,7 +211,6 @@ const emsesp_devicedata_2 = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// NETWORK
|
// NETWORK
|
||||||
app.get(NETWORK_STATUS_ENDPOINT, (req, res) => { res.json(network_status); });
|
app.get(NETWORK_STATUS_ENDPOINT, (req, res) => { res.json(network_status); });
|
||||||
app.get(NETWORK_SETTINGS_ENDPOINT, (req, res) => { res.json(network_settings); });
|
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(FACTORY_RESET_ENDPOINT, (req, res) => { res.sendStatus(200); });
|
||||||
app.post(UPLOAD_FIRMWARE_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.post(SIGN_IN_ENDPOINT, (req, res) => { res.json(signin); });
|
||||||
|
app.get(GENERATE_TOKEN_ENDPOINT, (req, res) => { res.json(generate_token); });
|
||||||
|
|
||||||
// EMS-ESP Project stuff
|
// EMS-ESP Project stuff
|
||||||
app.get(EMSESP_SETTINGS_ENDPOINT, (req, res) => { res.json(emsesp_settings); });
|
app.get(EMSESP_SETTINGS_ENDPOINT, (req, res) => { res.json(emsesp_settings); });
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#define EMSESP_APP_VERSION "3.0.3b2"
|
#define EMSESP_APP_VERSION "3.0.3b3"
|
||||||
#define EMSESP_PLATFORM "ESP32"
|
#define EMSESP_PLATFORM "ESP32"
|
||||||
|
|||||||
Reference in New Issue
Block a user