8000 add generate token endpoint and ui for generating tokens for users · rjwats/esp8266-react@59575ed · GitHub
[go: up one dir, main page]

Skip to content

Commit 59575ed

Browse files
committed
add generate token endpoint and ui for generating tokens for users
1 parent 6e22893 commit 59575ed

9 files changed

+135
-2
lines changed

interface/src/api/Endpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ export const SYSTEM_STATUS_ENDPOINT = ENDPOINT_ROOT + "systemStatus";
1818
export const SIGN_IN_ENDPOINT = ENDPOINT_ROOT + "signIn";
1919
export const VERIFY_AUTHORIZATION_ENDPOINT = ENDPOINT_ROOT + "verifyAuthorization";
2020
export const SECURITY_SETTINGS_ENDPOINT = ENDPOINT_ROOT + "securitySettings";
21+
export const GENERATE_TOKEN_ENDPOINT = ENDPOINT_ROOT + "generateToken";
2122
export const RESTART_ENDPOINT = ENDPOINT_ROOT + "restart";
2223
export const FACTORY_RESET_ENDPOINT = ENDPOINT_ROOT + "factoryReset";

interface/src/ntp/NTPStatusForm.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ class NTPStatusForm extends Component<NTPStatusFormProps, NTPStatusFormState> {
9090
<Dialog
9191
open={this.state.settingTime}
9292
onClose={this.closeSetTime}
93+
fullWidth
94+
maxWidth="sm"
9395
>
9496
<DialogTitle>Set Time</DialogTitle>
9597
<DialogContent dividers>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { Fragment } from 'react';
2+
import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextareaAutosize, TextField } from '@material-ui/core';
3+
4+
import { FormButton } from '../components';
5+
import { redirectingAuthorizedFetch } from '../authentication';
6+
import { GENERATE_TOKEN_ENDPOINT } from '../api';
7+
import { withSnackbar, WithSnackbarProps } from 'notistack';
8+
9+
interface GenerateTokenProps extends WithSnackbarProps {
10+
username: string;
11+
onClose: () => void;
12+
}
13+
14+
interface GenerateTokenState {
15+
token?: string;
16+
}
17+
18+
class GenerateToken extends React.Component<GenerateTokenProps, GenerateTokenState> {
19+
20+
state: GenerateTokenState = {};
21+
22+
componentDidMount() {
23+
const { username } = this.props;
24+
redirectingAuthorizedFetch(GENERATE_TOKEN_ENDPOINT + "?" + new URLSearchParams({ username }), { method: 'GET' })
25+
.then(response => {
26+
if (response.status === 200) {
27+
return response.json();
28+
} else {
29+
throw Error("Error generating token: " + response.status);
30+
}
31+
}).then(generatedToken => {
32+
console.log(generatedToken);
33+
this.setState({ token: generatedToken.token });
34+
})
35+
.catch(error => {
36+
this.props.enqueueSnackbar(error.message || "Problem generating token", { variant: 'error' });
37+
});
38+
}
39+
40+
render() {
41+
const { onClose, username } = this.props;
42+
const { token } = this.state;
43+
return (
44+
<Dialog onClose={onClose} aria-labelledby="generate-token-dialog-title" open fullWidth maxWidth="sm">
45+
<DialogTitle id="generate-token-dialog-title">Token for: {username}</DialogTitle>
46+
<DialogContent dividers>
47+
{token ?
48+
<Fragment>
49+
<Box bgcolor="primary.main" color="primary.contrastText" p={2} mt={2} mb={2}>
50+
<Typography variant="body1">
51+
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.
52+
</Typography>
53+
</Box>
54+
<Box mt={2} mb={2}>
55+
<TextField label="Token" multiline value={token} fullWidth contentEditable={false} />
56+
</Box>
57+
</Fragment>
58+
:
59+
<Box m={4} textAlign="center">
60+
<LinearProgress />
61+
<Typography variant="h6">
62+
Generating token&hellip;
63+
</Typography>
64+
</Box>
65+
}
66+
</DialogContent>
67+
<DialogActions>
68+
<FormButton variant="contained" color="primary" type="submit" onClick={onClose}>
69+
Close
70+
</FormButton>
71+
</DialogActions>
72+
</Dialog>
73+
);
74+
}
75+
}
76+
77+
export default withSnackbar(GenerateToken);

interface/src/security/ManageUsersForm.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import CheckIcon from '@material-ui/icons/Check';
1111
import IconButton from '@material-ui/core/IconButton';
1212
import SaveIcon from '@material-ui/icons/Save';
1313
import PersonAddIcon from '@material-ui/icons/PersonAdd';
14+
import VpnKeyIcon from '@material-ui/icons/VpnKey';
1415

1516
import { withAuthenticatedContext, AuthenticatedContextProps } from '../authentication';
1617
import { RestFormProps, FormActions, FormButton, extractEventValue } from '../components';
1718

1819
import UserForm from './UserForm';
1920
import { SecuritySettings, User } from './types';
21+
import GenerateToken from './GenerateToken';
2022

2123
function compareUsers(a: User, b: User) {
2224
if (a.username < b.username) {
@@ -33,6 +35,7 @@ type ManageUsersFormProps = RestFormProps<SecuritySettings> & AuthenticatedConte
3335
type ManageUsersFormState = {
3436
creating: boolean;
3537
user?: User;
38+
generateTokenFor?: string;
3639
}
3740

3841
class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersFormState> {
@@ -66,6 +69,18 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
6669
this.props.setData({ ...data, users });
6770
}
6871

72+
closeGenerateToken = () => {
73+
this.setState({
74+
generateTokenFor: undefined
75+
});
76+
}
77+
78+
generateToken = (user: User) => {
79+
this.setState({
80+
generateTokenFor: user.username
81+
});
82+
}
83+
6984
startEditingUser = (user: User) => {
7085
this.setState({
7186
creating: false,
@@ -96,14 +111,15 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
96111
this.setState({ user: { ...this.state.user!, [name]: extractEventValue(event) } });
97112
};
98113

114+
99115
onSubmit = () => {
100116
this.props.saveData();
101117
this.props.authenticatedContext.refresh();
102118
}
103119

104120
render() {
105121
const { width, data } = this.props;
106-
const { user, creating } = this.state;
122+
const { user, creating, generateTokenFor } = this.state;
107123
return (
108124
<Fragment>
109125
<ValidatorForm onSubmit={this.onSubmit}>
@@ -127,6 +143,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
127143
}
128144
</TableCell>
129145
<TableCell align="center">
146+
<IconButton size="small" aria-label="Generate Token" onClick={() => this.generateToken(user)}>
147+
<VpnKeyIcon />
148+
</IconButton>
130149
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
131150
<DeleteIcon />
132151
</IconButton>
@@ -164,6 +183,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
164183
</FormButton>
165184
</FormActions>
166185
</ValidatorForm>
186+
{
187+
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
188+
}
167189
{
168190
user &&
169191
<UserForm

interface/src/security/UserForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class UserForm extends React.Component<UserFormProps> {
3232
const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props;
3333
return (
3434
<ValidatorForm onSubmit={onDoneEditing} ref={this.formRef}>
35-
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open>
35+
<Dialog onClose={onCancelEditing} aria-labelledby="user-form-dialog-title" open fullWidth maxWidth="sm">
3636
<DialogTitle id="user-form-dialog-title">{creating ? 'Add' : 'Modify'} User</DialogTitle>
3737
<DialogContent dividers>
3838
<TextValidator

interface/src/security/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ export interface SecuritySettings {
99
jwt_secret: string;
1010
}
1111

12+
export interface GeneratedToken {
13+
token: string;
14+
}

interface/src/system/SystemStatusForm.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
121121
<Dialog
122122
open={this.state.confirmRestart}
123123
onClose={this.onRestartRejected}
124+
fullWidth
125+
maxWidth="sm"
124126
>
125127
<DialogTitle>Confirm Restart</DialogTitle>
126128
<DialogContent dividers>
@@ -168,6 +170,8 @@ class SystemStatusForm extends Component<SystemStatusFormProps, SystemStatusForm
168170
<Dialog
169171
open={this.state.confirmFactoryReset}
170172
onClose={this.onFactoryResetRejected}
173+
fullWidth
174+
maxWidth="sm"
171175
>
172176
<DialogTitle>Confirm Factory Reset</DialogTitle>
173177
<DialogContent dividers>

lib/framework/SecuritySettingsService.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs)
77
_fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE),
88
_jwtHandler(FACTORY_JWT_SECRET) {
99
addUpdateHandler([&](const String& originId) { configureJWTHandler(); }, false);
10+
server->on(GENERATE_TOKEN_PATH,
11+
HTTP_GET,
12+
wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1),
13+
AuthenticationPredicates::IS_ADMIN));
1014
}
1115

1216
void SecuritySettingsService::begin() {
@@ -34,6 +38,21 @@ void SecuritySettingsService::configureJWTHandler() {
3438
_jwtHandler.setSecret(_state.jwtSecret);
3539
}
3640

41+
void SecuritySettingsService::generateToken(AsyncWebServerRequest* request) {
42+
AsyncWebParameter* usernameParam = request->getParam("username");
43+
for (User _user : _state.users) {
44+
if (_user.username == usernameParam->value()) {
45+
AsyncJsonResponse* response = new AsyncJsonResponse(false, GENERATE_TOKEN_SIZE);
46+
JsonObject root = response->getRoot();
47+
root["token"] = generateJWT(&_user);
48+
response->setLength();
49+
request->send(response);
50+
return;
51+
}
52+
}
53+
request->send(401);
54+
}
55+
3756
Authentication SecuritySettingsService::authenticateJWT(String& jwt) {
3857
DynamicJsonDocument payloadDocument(MAX_JWT_SIZE);
3958
_jwtHandler.parseJWT(jwt, payloadDocument);

lib/framework/SecuritySettingsService.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
#define SECURITY_SETTINGS_FILE "/config/securitySettings.json"
2626
#define SECURITY_SETTINGS_PATH "/rest/securitySettings"
2727

28+
#define GENERATE_TOKEN_SIZE 512
29+
#define GENERATE_TOKEN_PATH "/rest/generateToken"
30+
2831
#if FT_ENABLED(FT_SECURITY)
2932

3033
class SecuritySettings {
@@ -83,6 +86,8 @@ class SecuritySettingsService : public StatefulService<SecuritySettings>, public
8386
FSPersistence<SecuritySettings> _fsPersistence;
8487
ArduinoJsonJWT _jwtHandler;
8588

89+
void generateToken(AsyncWebServerRequest* request);
90+
8691
void configureJWTHandler();
8792

8893
/*

0 commit comments

Comments
 (0)
0