From 59575ede29ae4bf9067a2fddd570f4bbd30558cc Mon Sep 17 00:00:00 2001 From: Rick Watson Date: Wed, 30 Dec 2020 23:54:18 +0000 Subject: [PATCH] add generate token endpoint and ui for generating tokens for users --- interface/src/api/Endpoints.ts | 1 + interface/src/ntp/NTPStatusForm.tsx | 2 + interface/src/security/GenerateToken.tsx | 77 ++++++++++++++++++++++ interface/src/security/ManageUsersForm.tsx | 24 ++++++- interface/src/security/UserForm.tsx | 2 +- interface/src/security/types.ts | 3 + interface/src/system/SystemStatusForm.tsx | 4 ++ lib/framework/SecuritySettingsService.cpp | 19 ++++++ lib/framework/SecuritySettingsService.h | 5 ++ 9 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 interface/src/security/GenerateToken.tsx diff --git a/interface/src/api/Endpoints.ts b/interface/src/api/Endpoints.ts index a0be3d46..42c5a126 100644 --- a/interface/src/api/Endpoints.ts +++ b/interface/src/api/Endpoints.ts @@ -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"; diff --git a/interface/src/ntp/NTPStatusForm.tsx b/interface/src/ntp/NTPStatusForm.tsx index 6b277dd1..aadfb6e7 100644 --- a/interface/src/ntp/NTPStatusForm.tsx +++ b/interface/src/ntp/NTPStatusForm.tsx @@ -90,6 +90,8 @@ class NTPStatusForm extends Component { Set Time diff --git a/interface/src/security/GenerateToken.tsx b/interface/src/security/GenerateToken.tsx new file mode 100644 index 00000000..86cb4583 --- /dev/null +++ b/interface/src/security/GenerateToken.tsx @@ -0,0 +1,77 @@ +import React, { Fragment } from 'react'; +import { Dialog, DialogTitle, DialogContent, DialogActions, Box, LinearProgress, Typography, TextareaAutosize, 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 { + + 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 ( + + Token for: {username} + + {token ? + + + + 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. + + + + + + + : + + + + Generating token… + + + } + + + + Close + + + + ); + } +} + +export default withSnackbar(GenerateToken); diff --git a/interface/src/security/ManageUsersForm.tsx b/interface/src/security/ManageUsersForm.tsx index b8c63b7f..32cdff74 100644 --- a/interface/src/security/ManageUsersForm.tsx +++ b/interface/src/security/ManageUsersForm.tsx @@ -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 & AuthenticatedConte type ManageUsersFormState = { creating: boolean; user?: User; + generateTokenFor?: string; } class ManageUsersForm extends React.Component { @@ -66,6 +69,18 @@ class ManageUsersForm extends React.Component { + this.setState({ + generateTokenFor: undefined + }); + } + + generateToken = (user: User) => { + this.setState({ + generateTokenFor: user.username + }); + } + startEditingUser = (user: User) => { this.setState({ creating: false, @@ -96,6 +111,7 @@ class ManageUsersForm extends React.Component { this.props.saveData(); this.props.authenticatedContext.refresh(); @@ -103,7 +119,7 @@ class ManageUsersForm extends React.Component @@ -127,6 +143,9 @@ class ManageUsersForm extends React.Component + this.generateToken(user)}> + + this.removeUser(user)}> @@ -164,6 +183,9 @@ class ManageUsersForm extends React.Component + { + generateTokenFor && + } { user && { const { user, creating, handleValueChange, onDoneEditing, onCancelEditing } = this.props; return ( - + {creating ? 'Add' : 'Modify'} User Confirm Restart @@ -168,6 +170,8 @@ class SystemStatusForm extends Component Confirm Factory Reset diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index 87027db5..4a73d2bc 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -7,6 +7,10 @@ SecuritySettingsService::SecuritySettingsService(AsyncWebServer* server, FS* fs) _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() { @@ -34,6 +38,21 @@ void SecuritySettingsService::configureJWTHandler() { _jwtHandler.setSecret(_state.jwtSecret); } +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); +} + Authentication SecuritySettingsService::authenticateJWT(String& jwt) { DynamicJsonDocument payloadDocument(MAX_JWT_SIZE); _jwtHandler.parseJWT(jwt, payloadDocument); diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 236bfe4e..9693e7fb 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -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, public FSPersistence _fsPersistence; ArduinoJsonJWT _jwtHandler; + void generateToken(AsyncWebServerRequest* request); + void configureJWTHandler(); /*