Skip to content

Commit

Permalink
Merge pull request #364 from WormBase/secure-api-access
Browse files Browse the repository at this point in the history
Added API token usage and general activity tracking
  • Loading branch information
mluypaert authored Jan 10, 2024
2 parents 14adfc3 + 539a89d commit e3d7d0e
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 59 deletions.
10 changes: 6 additions & 4 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import Main from './containers/Main';
import { theme, MuiThemeProvider } from './components/elements';

export default () => (
<GoogleOAuthProvider clientId={process.env.REACT_APP_GOOGLE_OAUTH_CLIENT_ID}>
<React.StrictMode>
<React.StrictMode>
<GoogleOAuthProvider
clientId={process.env.REACT_APP_GOOGLE_OAUTH_CLIENT_ID}
>
<BrowserRouter>
<Authenticate>
<EntityTypesContextProvider>
Expand All @@ -18,6 +20,6 @@ export default () => (
</EntityTypesContextProvider>
</Authenticate>
</BrowserRouter>
</React.StrictMode>
</GoogleOAuthProvider>
</GoogleOAuthProvider>
</React.StrictMode>
);
4 changes: 2 additions & 2 deletions client/src/components/elements/SpeciesSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useDataFetch } from '../../containers/Authenticate';

const SpeciesSelect = (props) => {
const memoizedFetchFunc = useCallback(
(authorizedFetch) =>
(fetchFn) =>
mockFetchOrNot(
(mockFetch) => {
return mockFetch.get('*', [
Expand All @@ -27,7 +27,7 @@ const SpeciesSelect = (props) => {
]);
},
() =>
authorizedFetch(`/api/species`, {
fetchFn(`/api/species`, {
method: 'GET',
})
),
Expand Down
1 change: 0 additions & 1 deletion client/src/containers/Authenticate/Profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Profile.propTypes = {
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
onLogout: PropTypes.func.isRequired,
children: PropTypes.element,
};

Expand Down
148 changes: 118 additions & 30 deletions client/src/containers/Authenticate/TokenMgmt.js
Original file line number Diff line number Diff line change
@@ -1,68 +1,156 @@
import React, { useContext, useReducer } from 'react';
import { Button } from '../../components/elements';
import React, { useContext, useReducer, useEffect } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';

import { Button } from '../../components/elements';
import AuthorizationContext from '../../containers/Authenticate/AuthorizationContext';
import { useCallback } from 'react';

const ACTION_STORE = 'STORE';
const ACTION_REVOKE = 'REVOKE';
const UPDATE_METADATA = 'UPDATE_METADATA';

function tokenMetaDataReducer(state, action) {
let newState = { ...state };

switch (action.type) {
case UPDATE_METADATA:
console.log('Metadata update trigerred.');
newState = { ...action['payload'] };
break;
default:
console.log('Invalid action type detected:');
console.log(action.type);
throw new Error();
}

function TokenMgmt() {
const ACTION_STORE = 'STORE';
const ACTION_REVOKE = 'REVOKE';
return newState;
}

function tokenReducer(state, action) {
const newState = { ...state };

switch (action.type) {
case ACTION_STORE:
newState['apiToken'] = action.payload;
break;
case ACTION_REVOKE:
newState['apiToken'] = null;
break;
default:
console.log('Invalid action type detected:');
console.log(action.type);
throw new Error();
}

return newState;
}

function TokenMgmt() {
const { authorizedFetch, user } = useContext(AuthorizationContext);

const [tokenState, dispatchTokenState] = useReducer(tokenReducer, {
apiToken: null,
});

const defaultTokenInstructions =
'No stored ID token to display.\n' +
"Click the 'Store token' button below to store the current ID token and display it here.";

function tokenReducer(state, action) {
const newState = { ...state };

switch (action.type) {
case ACTION_STORE:
newState['apiToken'] = user.id_token;
break;
case ACTION_REVOKE:
newState['apiToken'] = null;
break;
default:
console.log('Invalid action type detected:');
console.log(action.type);
throw new Error();
const [tokenMetaDataState, dispatchTokenMetaData] = useReducer(
tokenMetaDataReducer,
{
'token-stored?': false,
'last-used': null,
}
);

return newState;
}
const updateTokenMetadata = useCallback(
() => {
authorizedFetch('/api/auth/token-metadata', { method: 'GET' })
.then((response) => {
if (response.ok) {
return response.json();
} else {
console.log(
'Error while retrieving token metadata. Returned response: ',
response
);
throw new Error('Error while retrieving token metadata');
}
})
.then((data) => {
console.log('token-metadata result received:', data);

dispatchTokenMetaData({ type: UPDATE_METADATA, payload: data });
})
.catch((error) => {
console.log(
'Error caught on authorizedFetch for token-metadata:',
error
);
});
},
[authorizedFetch]
);

const noTokenInstructions =
'No stored ID token to display.\n' +
"Click the 'Store token' button below to store the current ID token and display it here.";
const newTokenInstructions =
'Stored tokens can not be retrieved for display after storage.\n' +
"Click the 'Store token' button below to store a new token (invalidating the current stored token) and display it here.";

function storeTokenHandler() {
console.log('storeTokenHandler triggered.');

authorizedFetch(`/api/auth/token`, {
method: 'POST',
}).then((response) => {
if (response.ok) {
dispatchTokenState({ type: ACTION_STORE, payload: user.id_token });
} else {
console.log('Error returned by /auth/token POST endpoint.');
throw new Error('API endpoint for token storage returned error.');
}
});

dispatchTokenState({ type: ACTION_STORE });
}

function revokeTokenHandler() {
console.log('revokeTokenHandler triggered.');

authorizedFetch(`/api/auth/token`, {
method: 'DELETE',
}).then((response) => {
if (response.ok) {
dispatchTokenState({ type: ACTION_REVOKE });
} else {
console.log('Error returned by /auth/token DELETE endpoint.');
throw new Error('API endpoint for token revoking returned error.');
}
});

dispatchTokenState({ type: ACTION_REVOKE });
}

useEffect(
() => {
updateTokenMetadata();
},
[tokenState, updateTokenMetadata]
);

return (
<div>
<span>
Token stored?: {tokenMetaDataState['token-stored?'] ? 'Yes' : 'No'}
</span>
<br />
<span>Token last used: {tokenMetaDataState['last-used'] || 'Never'}</span>
<br />
<textarea
disabled={true}
style={{ width: '100%', height: 65 }}
value={tokenState.apiToken || defaultTokenInstructions}
value={
tokenState.apiToken
? tokenState.apiToken
: tokenMetaDataState['token-stored?']
? newTokenInstructions
: noTokenInstructions
}
/>
<CopyToClipboard text={tokenState.apiToken}>
<div>
Expand Down
8 changes: 5 additions & 3 deletions client/src/containers/Main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ function Main({ classes }) {
component={() => (
<DocumentTitle title="Your profile">
<Profile {...user}>
<TokenMgmt />
<br />
<Logout onLogout={handleLogout} />
<>
<TokenMgmt />
<br />
<Logout onLogout={handleLogout} />
</>
</Profile>
</DocumentTitle>
)}
Expand Down
12 changes: 12 additions & 0 deletions resources/schema/definitions.edn
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@
:cardinality :db.cardinality/one
:unique :db.unique/value
:noHistory true}
#:db{:ident :person/auth-token-stored-at
:valueType :db.type/instant
:cardinality :db.cardinality/one
:doc "When the current auth-token was stored."}
#:db{:ident :person/auth-token-last-used
:valueType :db.type/instant
:cardinality :db.cardinality/one
:doc "When the stored auth-token was last used to access the API."}
#:db{:ident :person/last-activity
:valueType :db.type/instant
:cardinality :db.cardinality/one
:doc "When the user last showed any activity in the NS (through either API or web)."}
#:db{:ident :person/name
:valueType :db.type/string
:cardinality :db.cardinality/one
Expand Down
Loading

0 comments on commit e3d7d0e

Please sign in to comment.