Skip to content

Commit

Permalink
Explicit token refresh for oauth2 token page (#23)
Browse files Browse the repository at this point in the history
- do not generate and invalidate the token on page load as it makes it too easy to happen accidentally
- instead regenerate the token on explicit button press
- adjusted some UI texts to more accurately explain the behaviour

Co-authored-by: Marco Herglotz <marco.herglotz@cas.de>
  • Loading branch information
herglotzmarco and Marco Herglotz authored Dec 20, 2024
1 parent 938bd8d commit 43c8e9c
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 32 deletions.
85 changes: 60 additions & 25 deletions src/frontend/src/components/OAuth2ProxyApiTokenComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,71 @@ import {
} from '@sonatype/nexus-ui-plugin';

export default function OAuth2ProxyApiTokenComponent() {
const [token, setToken] = React.useState('');
const [error, setError] = React.useState(false);

React.useEffect(() => {
(async () => {
try {
const reponse = await Axios.post("/service/rest/oauth2-proxy/user/reset-token");
setToken(reponse.data);
} catch (e) {
setError(true);
console.error('Failed to reset token:', e);
}
})();
}, []);

if (error) {
return <div>Error fetching user data.</div>;
}

return <Page>
<PageHeader>
<PageTitle icon={faKey} text="OAuth2 Proxy API Token" description="Access your API token for non-interactive access"></PageTitle>
</PageHeader>
<ContentBody>
<Section>
<p>A new API token has been created. It is only displayed once. Store it in a safe place!</p>
<p>⚠️ The next time you visit this page, you will automatically reset the token again.</p>
<p>💡 Make sure no one was watching when displaying this token. If in doubt, just reset it once more.</p>
<SectionFooter>Your API token: {token}</SectionFooter>
</Section>
<TokenSection />
</ContentBody>
</Page >
}

function TokenSection() {
const [token, setToken] = React.useState('****************************************');
const [resetFailed, setResetFailed] = React.useState(false);
const [resetInProgress, setResetInProgress] = React.useState(false);
const [tokenFreshlyReset, setTokenFreshlyReset] = React.useState(false);

const resetToken = React.useCallback(async () => {
if(resetInProgress) {
console.log("Still resetting the token, not sending another request now");
} else {
setResetInProgress(true);
Axios.post("/service/rest/oauth2-proxy/user/reset-token")
.then(response => {
setToken(response.data);
setTokenFreshlyReset(true);
setResetInProgress(false);
})
.catch(error => {
setResetFailed(true);
console.error('Failed to reset token:' + JSON.stringify(error.toJSON()));
setResetInProgress(false);
});
}
}, [resetInProgress])

if(resetFailed) {
return <Section>
<p>⛔ Failed to generate a new access token</p>
</Section>
}

if(tokenFreshlyReset) {
return <Section>
<p>✔ A new API token has been created. It is only displayed once. Store it in a safe place!</p>
<p>⚠️ The old API token has been invalidated</p>
<p>💡 Make sure no one was watching when displaying this token. If in doubt, just reset it once more.</p>
<TokenFooter resetInProgress={resetInProgress} resetPressed={() => resetToken()} token={token}/>
</Section>
}

return <Section>
<p>⚠️ Your current API token is hidden. Click the button to generate a new token</p>
<p>⚠️ When a new token is generated, the old one is invalidated immediately</p>
<TokenFooter resetInProgress={resetInProgress} resetPressed={() => resetToken()} token={token}/>
</Section>
}

function TokenFooter({resetInProgress, resetPressed, token}) {
const buttonStyle = {
marginRight: '1em',
minWidth: '10em'
}
let buttonText = resetInProgress ? "Generating..." : "Regenerate Token"
return <SectionFooter>
<button disabled={resetInProgress} onClick={resetPressed} style={buttonStyle}>{buttonText}</button>
<span>Your API token: {token}</span>
</SectionFooter>
}
32 changes: 25 additions & 7 deletions src/frontend/src/components/OAuth2ProxyApiTokenComponent.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import OAuth2ProxyApiTokenComponent from './OAuth2ProxyApiTokenComponent';

import '@testing-library/jest-dom'
import { act, render } from '@testing-library/react';
import { act, render, fireEvent } from '@testing-library/react';

jest.mock('axios');

Expand All @@ -24,19 +24,37 @@ describe('OAuth2ProxyApiTokenComponent', () => {
Axios.post.mockImplementationOnce(() => Promise.resolve({ data: 'foobar' }));
});

it('calls post request once when rendered', async () => {
it('renders page without immediately re-generating the token', async () => {
const { findByText } = render(<OAuth2ProxyApiTokenComponent />);

const token = await findByText(/Your current API token is hidden/i);
expect(token).toBeInTheDocument();

const button = await findByText(/Regenerate Token/i);
expect(button).toBeInTheDocument();

expect(Axios.post).toHaveBeenCalledTimes(0);
});

it('calls post request once when regenerate button is clicked', async () => {
await act(async () => {
render(<OAuth2ProxyApiTokenComponent />);
const { findByText } = render(<OAuth2ProxyApiTokenComponent />);
const button = await findByText(/Regenerate Token/i);
fireEvent.click(button);
});

expect(Axios.post).toHaveBeenCalledTimes(1);
expect(Axios.post).toHaveBeenCalledWith('/service/rest/oauth2-proxy/user/reset-token');
});

it('renders response content as token', async () => {
const { findByText } = render(<OAuth2ProxyApiTokenComponent />);

const token = await findByText(/foobar/i);
expect(token).toBeInTheDocument();
await act(async () => {
const { findByText } = render(<OAuth2ProxyApiTokenComponent />);
const button = await findByText(/Regenerate Token/i);
fireEvent.click(button);

const token = await findByText(/foobar/i);
expect(token).toBeInTheDocument();
});
});
});

0 comments on commit 43c8e9c

Please sign in to comment.