Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

[Web Portal] revoke browser tokens when user changes password / logout #3835

Merged
merged 1 commit into from
Nov 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/rest-server/src/controllers/v2/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// module dependencies
const jwt = require('jsonwebtoken');
const userModel = require('@pai/models/v2/user');
const createError = require('@pai/utils/error');
const authConfig = require('@pai/config/authn');
const logger = require('@pai/config/logger');
const groupModel = require('@pai/models/v2/group');
const vcModel = require('@pai/models/v2/virtual-cluster');
const tokenModel = require('@pai/models/token');

const getUserVCs = async (username) => {
const userInfo = await userModel.getUser(username);
Expand Down Expand Up @@ -344,6 +347,16 @@ const updateUserPassword = async (req, res, next) => {
if (req.user.admin || newUserValue['password'] === userValue['password']) {
newUserValue['password'] = newPassword;
await userModel.updateUser(username, newUserValue, true);
// try to revoke browser tokens
try {
await tokenModel.batchRevoke(username, (token) => {
const data = jwt.decode(token);
return !data.application;
});
} catch (err) {
logger.error('Failed to revoke tokens after password is updated', err);
// pass
}
return res.status(201).json({
message: 'update user password successfully.',
});
Expand Down
12 changes: 12 additions & 0 deletions src/rest-server/src/models/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ const revoke = async (token) => {
await k8sSecret.replace(namespace, username, result);
};

const batchRevoke = async (username, filter) => {
const item = await k8sSecret.get(namespace, username);
const result = purge(item || {});
for (const [key, val] of Object.entries(result)) {
if (filter(val)) {
delete result[key];
}
}
await k8sSecret.replace(namespace, username, result);
};

const verify = async (token) => {
const payload = jwt.verify(token, secret);
const username = payload.username;
Expand All @@ -141,6 +152,7 @@ const verify = async (token) => {
module.exports = {
list,
create,
batchRevoke,
revoke,
verify,
};
9 changes: 7 additions & 2 deletions src/webportal/src/app/user/fabric/conn.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import cookies from 'js-cookie';
import config from '../../config/webportal.config';
import { checkToken } from '../user-auth/user-auth.component';
import { clearToken } from '../user-logout/user-logout.component';
Expand Down Expand Up @@ -86,17 +87,21 @@ export const createUserRequest = async (
export const updateUserPasswordRequest = async (
username,
newPassword,
oldPassword = '',
oldPassword = undefined,
) => {
const url = `${config.restServerUri}/api/v2/user/${username}/password`;
const token = checkToken();
return fetchWrapper(url, {
const result = await fetchWrapper(url, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ newPassword, oldPassword }),
});
if (username === cookies.get('user')) {
clearToken();
}
return result;
};

export const updateUserEmailRequest = async (username, email) => {
Expand Down
4 changes: 4 additions & 0 deletions src/webportal/src/app/user/fabric/user-profile/header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ const UserProfileHeader = ({ userInfo, onEditProfile, onEditPassword }) => {
>
<div>
<div>
If password is changed, all browser tokens will be revoked and
you will be logged out.
</div>
<div className={t.mt3}>
<TextField
label='Old Password'
componentRef={oldPasswordRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function BatchPasswordEditor({ isOpen = false, hide }) {
};

const tdPaddingStyle = c(t.pa3);
const tdLabelStyle = c(tdPaddingStyle, t.tr);
const tdLabelStyle = c(tdPaddingStyle, t.tr, t.vTop);

const { spacing } = getTheme();

Expand All @@ -114,6 +114,7 @@ export default function BatchPasswordEditor({ isOpen = false, hide }) {
<CustomPassword
componentRef={passwordRef}
placeholder='Enter password'
description="User's browser tokens will be revoked if password is changed"
/>
</td>
</tr>
Expand Down
15 changes: 10 additions & 5 deletions src/webportal/src/app/user/fabric/userView/UserEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export default function UserEditor({
};

const tdPaddingStyle = c(t.pa3);
const tdLabelStyle = c(tdPaddingStyle, t.tr);
const tdLabelStyle = c(tdPaddingStyle, t.tr, t.vTop);

/**
* @type {import('office-ui-fabric-react').IDropdownOption[]}
Expand All @@ -242,7 +242,7 @@ export default function UserEditor({
<Modal
isOpen={isOpen}
isBlocking={true}
containerClassName={mergeStyles({ width: '450px', minWidth: '450px' })}
containerClassName={mergeStyles({ width: '480px', minWidth: '480px' })}
>
<div className={c(t.pa4)}>
<form onSubmit={handleSubmit}>
Expand All @@ -253,8 +253,10 @@ export default function UserEditor({
<table className={c(t.mlAuto, t.mrAuto)}>
<tbody>
<tr>
<td className={tdLabelStyle}>Name</td>
<td className={tdPaddingStyle} style={{ minWidth: '280px' }}>
<td className={tdLabelStyle} style={{ minWidth: '140px' }}>
Name
</td>
<td className={tdPaddingStyle} style={{ minWidth: '248px' }}>
<TextField
id={`NameInput${Math.random()}`}
componentRef={usernameRef}
Expand All @@ -270,6 +272,10 @@ export default function UserEditor({
<CustomPassword
componentRef={passwordRef}
placeholder={isCreate ? 'Enter password' : '******'}
description={
!isCreate &&
"User's browser tokens will be revoked if password is changed"
}
/>
</td>
</tr>
Expand All @@ -294,7 +300,6 @@ export default function UserEditor({
disabled={isAdmin}
onChange={handleVCsChanged}
placeholder='Select an option'
style={{ maxWidth: '248px' }}
/>
</td>
</tr>
Expand Down
11 changes: 11 additions & 0 deletions src/webportal/src/app/user/user-logout/user-logout.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,21 @@ const querystring = require('querystring');
const webportalConfig = require('../../config/webportal.config.js');

const userLogout = (origin = window.location.href) => {
// revoke token
const token = cookies.get('token');
const url = `${webportalConfig.restServerUri}/api/v1/token/${token}`;
fetch(url, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
}).catch(console.err);
// clear cookies
cookies.remove('user');
cookies.remove('token');
cookies.remove('admin');
cookies.remove('my-jobs');
// redirect
if (webportalConfig.authnMethod === 'basic') {
if (!origin) {
window.location.replace('/index.html');
Expand Down