Skip to content

Commit bdca0f8

Browse files
Merge pull request #167 from Sanketika-Obsrv/keycloak_user_fix
#OBS-I529: Keycloak authentication and user creation fix
2 parents ced6fe5 + 9d86470 commit bdca0f8

File tree

6 files changed

+133
-23
lines changed

6 files changed

+133
-23
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NextFunction, Request, Response } from "express";
2+
import appConfig from "../../shared/resources/appConfig";
3+
import { datasetServiceHttpInstance } from "../services/dataset";
4+
5+
const authenticationType = appConfig.AUTHENTICATION_TYPE;
6+
7+
export default {
8+
name: 'datasetAuthInjector',
9+
handler: () => (request: any, response: Response, next: NextFunction) => {
10+
if (authenticationType === 'keycloak') {
11+
const keycloakToken = JSON.parse(request?.session['keycloak-token']);
12+
const access_token: string = keycloakToken.access_token;
13+
datasetServiceHttpInstance.defaults.headers['Authorization'] = `Bearer ${access_token}`;
14+
} else if (authenticationType === 'basic') {
15+
const jwtToken: string = request?.session?.token;
16+
datasetServiceHttpInstance.defaults.headers['Authorization'] = `Bearer ${jwtToken}`;
17+
}
18+
next();
19+
}
20+
};

src/main/resources/routesConfig.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import passportAuthenticateCallback from '../middlewares/passportAuthenticate';
99
import setContext from '../middlewares/setContext';
1010
import authorizationMiddleware from '../middlewares/authorization';
1111
import { permissions } from '../middlewares/authorization';
12+
import datasetAuthInjector from '../middlewares/datasetAuthInjector';
1213

1314
const baseURL = appConfig.BASE_URL;
1415
export default [
@@ -146,6 +147,8 @@ export default [
146147
path: 'state/:datasetId',
147148
method: 'GET',
148149
middlewares: [
150+
ensureLoggedInMiddleware,
151+
datasetAuthInjector.handler(),
149152
controllers.get('dataset:state')?.handler({})
150153
]
151154
},
@@ -154,6 +157,7 @@ export default [
154157
method: 'GET',
155158
middlewares: [
156159
ensureLoggedInMiddleware,
160+
datasetAuthInjector.handler(),
157161
controllers.get('dataset:diff')?.handler({})
158162
]
159163
},
@@ -162,6 +166,7 @@ export default [
162166
method: 'GET',
163167
middlewares: [
164168
ensureLoggedInMiddleware,
169+
datasetAuthInjector.handler(),
165170
controllers.get('dataset:exists')?.handler({})
166171
]
167172
}
@@ -174,6 +179,8 @@ export default [
174179
path: 'generate-fields/:dataset_id',
175180
method: 'GET',
176181
middlewares: [
182+
ensureLoggedInMiddleware,
183+
datasetAuthInjector.handler(),
177184
controllers.get('get:all:fields')?.handler({})
178185
]
179186
}

src/main/services/dataset.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import appConfig from '../../shared/resources/appConfig'
44
import { fieldsByStatus } from '../controllers/dataset_diff';
55
type Payload = Record<string, any>;
66

7-
const datasetServiceHttpInstance = axios.create({ baseURL: appConfig.OBS_API.URL});
7+
export const datasetServiceHttpInstance = axios.create({ baseURL: appConfig.OBS_API.URL});
88

99
const transform = (response: any) => _.get(response, 'data.result')
1010

src/main/services/keycloak.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ export const authenticated = async (request: any) => {
1818
request.session.preferred_username = preferred_username;
1919

2020
const user = await userService.find({ id: userId?.[0] });
21-
request.session.roles = user?.roles;
21+
request.session.userDetails = _.pick(user, ['id', 'user_name', 'email_address', 'roles', 'is_owner']);
22+
request.session.roles = _.get(user, ['roles']);
2223
} catch (err) {
2324
console.log('user not authenticated', request?.kauth?.grant?.access_token?.content?.sub, err);
2425
}
2526
};
2627

2728
export const deauthenticated = function (request: any) {
29+
delete request?.session?.userDetails;
2830
delete request?.session?.roles;
2931
delete request?.session?.userId;
3032
delete request?.session?.email_address;
@@ -44,6 +46,8 @@ export const userCreate = async (access_token: any, userRequest: any) => {
4446
const payload = {
4547
email: email_address,
4648
username: user_name,
49+
firstName: userRequest?.first_name,
50+
lastName: userRequest?.last_name,
4751
enabled: true,
4852
credentials: [
4953
{
@@ -63,7 +67,6 @@ export const userCreate = async (access_token: any, userRequest: any) => {
6367
.then((response) => {
6468
const location = _.get(response, 'headers.location');
6569
const userId = location ? _.last(location.split('/')) : null;
66-
console.log('keyuser', userId);
6770
if (!userId) {
6871
throw new Error('UserId not found');
6972
}

src/main/services/keycloakAuthProvider.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,21 @@ export class KeycloakAuthProvider implements BaseAuthProvider {
1515
return this.keycloak.middleware();
1616
}
1717

18-
authenticate(): (req: Request, res: Response, next: NextFunction) => void {
19-
return this.keycloak.protect();
18+
authenticate(): (req: any, res: Response, next: NextFunction) => void {
19+
const protect = this.keycloak.protect();
20+
21+
return async (req: any, res: Response, next: NextFunction) => {
22+
protect(req, res, async () => {
23+
try {
24+
if (req.kauth?.grant) {
25+
await authenticated(req);
26+
}
27+
next();
28+
} catch (error) {
29+
next(error);
30+
}
31+
});
32+
};
2033
}
2134

2235
async logout(req: Request, res: Response): Promise<void> {

web-console-v2/src/pages/UserManagement/AddUser.tsx

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React, { useEffect, useState } from 'react';
2-
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, MenuItem, Select, InputLabel, FormControl, Button, SelectChangeEvent } from '@mui/material';
2+
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, MenuItem, Select, InputLabel, FormControl, Button, SelectChangeEvent, InputAdornment, IconButton } from '@mui/material';
33
import { UserRequest } from './UserManagement';
44
import { useUserList } from 'services/user';
55
import { User } from './UserManagement';
66
import { useAlert } from 'contexts/AlertContextProvider';
77
import Alert from '@mui/material/Alert';
8+
import { Visibility, VisibilityOff } from '@mui/icons-material';
89

910
interface AddUserProps {
1011
open: boolean;
@@ -37,12 +38,20 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
3738
const { data: users } = useUserList();
3839
const { showAlert } = useAlert();
3940
const [error, setError] = useState<boolean | null>(null);
41+
const [passwordRequirements, setPasswordRequirements] = useState({
42+
length: false,
43+
uppercase: false,
44+
lowercase: false,
45+
number: false,
46+
specialChar: false,
47+
});
48+
const [showPassword, setShowPassword] = useState<boolean>(false);
4049

4150
useEffect(() => {
4251
const userName = newUser?.user_name.replace(/\s+/g, '_');
4352
const emailAddress = newUser?.email_address;
4453
if (userName || emailAddress) {
45-
const usernameExists = users?.data?.some((user: { user_name: string; }) => user.user_name === userName);
54+
const usernameExists = users?.data?.some((user: { user_name: string; }) => user.user_name.toLowerCase() === userName.toLowerCase());
4655
setIsUsernameTaken(usernameExists || false);
4756
} else {
4857
setIsUsernameTaken(null);
@@ -58,10 +67,53 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
5867

5968
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
6069
const { name, value } = e.target;
61-
setNewUser({
62-
...newUser,
63-
[name]: value,
64-
});
70+
71+
setNewUser(prev => ({
72+
...prev,
73+
[name]: name === 'user_name' ? value.toLowerCase() : value
74+
}));
75+
76+
if (name === 'password') {
77+
validatePassword(value);
78+
}
79+
};
80+
81+
const validatePassword = (password: string) => {
82+
const updatedRequirements = {
83+
length: password.length >= 8 && password.length <= 15,
84+
uppercase: /[A-Z]/.test(password),
85+
lowercase: /[a-z]/.test(password),
86+
number: /\d/.test(password),
87+
specialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password),
88+
};
89+
90+
setPasswordRequirements(updatedRequirements);
91+
};
92+
93+
const getPasswordHelperText = () => {
94+
const requirements = [];
95+
96+
if (!passwordRequirements.length) {
97+
requirements.push('8-15 characters');
98+
}
99+
if (!passwordRequirements.uppercase) {
100+
requirements.push('at least one uppercase letter');
101+
}
102+
if (!passwordRequirements.lowercase) {
103+
requirements.push('at least one lowercase letter');
104+
}
105+
if (!passwordRequirements.number) {
106+
requirements.push('at least one number');
107+
}
108+
if (!passwordRequirements.specialChar) {
109+
requirements.push('at least one special character');
110+
}
111+
112+
if (requirements.length > 0) {
113+
return `Password must contain: ${requirements.join(', ')}`;
114+
} else {
115+
return '';
116+
}
65117
};
66118

67119
const handleRoleChange = (e: SelectChangeEvent<string | string[]>) => {
@@ -74,15 +126,14 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
74126
const handleSubmit = () => {
75127
onSubmit(newUser)
76128
.then(() => {
77-
onClose();
78-
resetForm();
129+
onClose();
130+
resetForm();
79131
})
80132
.catch(() => {
81133
showAlert('Failed to create user', 'error');
82134
setError(true);
83135
});
84136
};
85-
86137

87138
const resetForm = () => {
88139
setNewUser({
@@ -93,18 +144,20 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
93144
});
94145
};
95146

147+
const isUserNameValid = newUser?.user_name && newUser?.user_name.length >= 3;
96148
const isEmailValid = newUser?.email_address ? emailRegex.test(newUser?.email_address) : true;
97-
const isFirstNameValid = !newUser.first_name || newUser.first_name.length >= 3;
98-
const isLastNameValid = !newUser.last_name || newUser.last_name.length >= 3;
149+
const isFirstNameValid = !newUser?.first_name || newUser?.first_name.length >= 3;
150+
const isLastNameValid = !newUser?.last_name || newUser?.last_name.length >= 3;
151+
const isPasswordValid = newUser?.password && getPasswordHelperText() === '';
99152
const isFormValid =
100-
newUser?.user_name &&
101153
newUser?.email_address &&
102-
newUser?.password &&
154+
isPasswordValid &&
103155
newUser?.roles.length > 0 &&
104156
isUsernameTaken === false &&
105157
isEmailValid &&
106158
isFirstNameValid &&
107-
isLastNameValid;
159+
isLastNameValid &&
160+
isUserNameValid;
108161

109162
const availableRoles = currentUser?.is_owner ? rolesOptions : rolesOptions.filter(role => role.value !== 'admin');
110163

@@ -137,8 +190,8 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
137190
onChange={handleChange}
138191
required
139192
margin="normal"
140-
error={isUsernameTaken === true}
141-
helperText={isUsernameTaken ? 'Username already exists' : ''}
193+
error={isUsernameTaken === true || isUserNameValid === false}
194+
helperText={isUsernameTaken ? 'Username already exists' : (isUserNameValid === false) ? 'Username must be at least 3 characters' : ''}
142195
/>
143196
<TextField
144197
label="First Name"
@@ -148,7 +201,7 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
148201
value={newUser.first_name}
149202
onChange={handleChange}
150203
margin="normal"
151-
error={ !isFirstNameValid}
204+
error={!isFirstNameValid}
152205
helperText={!isFirstNameValid ? 'If provided, first name must be at least 3 characters' : ''}
153206
/>
154207
<TextField
@@ -178,14 +231,28 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
178231
<TextField
179232
label="Password"
180233
name="password"
181-
type="password"
234+
type={showPassword ? 'text' : 'password'}
182235
fullWidth
183236
variant="outlined"
184237
value={newUser.password}
185238
onChange={handleChange}
186239
required
187240
margin="normal"
241+
error={isPasswordValid === false}
242+
helperText={isPasswordValid === false && getPasswordHelperText()}
188243
autoComplete="new-password"
244+
InputProps={{
245+
endAdornment: (
246+
<InputAdornment position="end">
247+
<IconButton
248+
onClick={() => setShowPassword(!showPassword)}
249+
edge="end"
250+
>
251+
{showPassword ? <Visibility /> : <VisibilityOff />}
252+
</IconButton>
253+
</InputAdornment>
254+
),
255+
}}
189256
/>
190257
<FormControl fullWidth margin="normal">
191258
<InputLabel>Role</InputLabel>

0 commit comments

Comments
 (0)