Skip to content

Commit a951ebc

Browse files
committed
feat: add generate token endpoint and ui for generating tokens for users
1 parent 8ea48f7 commit a951ebc

11 files changed

+1720
-6
lines changed

interface/src/api/Endpoints.ts

+1
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";
+77
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, 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

+22-1
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,
@@ -103,7 +118,7 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
103118

104119
render() {
105120
const { width, data } = this.props;
106-
const { user, creating } = this.state;
121+
const { user, creating, generateTokenFor } = this.state;
107122
return (
108123
<Fragment>
109124
<ValidatorForm onSubmit={this.onSubmit}>
@@ -125,6 +140,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
125140
{user.admin ? <CheckIcon /> : <CloseIcon />}
126141
</TableCell>
127142
<TableCell align="center">
143+
<IconButton size="small" aria-label="Generate Token" onClick={() => this.generateToken(user)}>
144+
<VpnKeyIcon />
145+
</IconButton>
128146
<IconButton size="small" aria-label="Delete" onClick={() => this.removeUser(user)}>
129147
<DeleteIcon />
130148
</IconButton>
@@ -162,6 +180,9 @@ class ManageUsersForm extends React.Component<ManageUsersFormProps, ManageUsersF
162180
</FormButton>
163181
</FormActions>
164182
</ValidatorForm>
183+
{
184+
generateTokenFor && <GenerateToken username={generateTokenFor} onClose={this.closeGenerateToken} />
185+
}
165186
{
166187
user &&
167188
<UserForm

interface/src/security/UserForm.tsx

+1-1
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

+3
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+
}

lib/framework/SecuritySettingsService.cpp

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer * server, FS * f
99
, _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE)
1010
, _jwtHandler(FACTORY_JWT_SECRET) {
1111
addUpdateHandler([&](const String & originId) { configureJWTHandler(); }, false);
12+
server->on(GENERATE_TOKEN_PATH, HTTP_GET, wrapRequest(std::bind(&SecuritySettingsService::generateToken, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
1213
}
1314

1415
void SecuritySettingsService::begin() {
@@ -109,6 +110,21 @@ ArJsonRequestHandlerFunction SecuritySettingsService::wrapCallback(ArJsonRequest
109110
};
110111
}
111112

113+
void SecuritySettingsService::generateToken(AsyncWebServerRequest* request) {
114+
AsyncWebParameter* usernameParam = request->getParam("username");
115+
for (User _user : _state.users) {
116+
if (_user.username == usernameParam->value()) {
117+
AsyncJsonResponse* response = new AsyncJsonResponse(false, GENERATE_TOKEN_SIZE);
118+
JsonObject root = response->getRoot();
119+
root["token"] = generateJWT(&_user);
120+
response->setLength();
121+
request->send(response);
122+
return;
123+
}
124+
}
125+
request->send(401);
126+
}
127+
112128
#else
113129

114130
User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true);

lib/framework/SecuritySettingsService.h

+5
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)