Skip to content

Commit

Permalink
Merge pull request #167 from Sanketika-Obsrv/keycloak_user_fix
Browse files Browse the repository at this point in the history
#OBS-I529: Keycloak authentication and user creation fix
  • Loading branch information
HarishGangula authored Jan 29, 2025
2 parents ced6fe5 + 9d86470 commit bdca0f8
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 23 deletions.
20 changes: 20 additions & 0 deletions src/main/middlewares/datasetAuthInjector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextFunction, Request, Response } from "express";
import appConfig from "../../shared/resources/appConfig";
import { datasetServiceHttpInstance } from "../services/dataset";

const authenticationType = appConfig.AUTHENTICATION_TYPE;

export default {
name: 'datasetAuthInjector',
handler: () => (request: any, response: Response, next: NextFunction) => {
if (authenticationType === 'keycloak') {
const keycloakToken = JSON.parse(request?.session['keycloak-token']);
const access_token: string = keycloakToken.access_token;
datasetServiceHttpInstance.defaults.headers['Authorization'] = `Bearer ${access_token}`;
} else if (authenticationType === 'basic') {
const jwtToken: string = request?.session?.token;
datasetServiceHttpInstance.defaults.headers['Authorization'] = `Bearer ${jwtToken}`;
}
next();
}
};
7 changes: 7 additions & 0 deletions src/main/resources/routesConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import passportAuthenticateCallback from '../middlewares/passportAuthenticate';
import setContext from '../middlewares/setContext';
import authorizationMiddleware from '../middlewares/authorization';
import { permissions } from '../middlewares/authorization';
import datasetAuthInjector from '../middlewares/datasetAuthInjector';

const baseURL = appConfig.BASE_URL;
export default [
Expand Down Expand Up @@ -146,6 +147,8 @@ export default [
path: 'state/:datasetId',
method: 'GET',
middlewares: [
ensureLoggedInMiddleware,
datasetAuthInjector.handler(),
controllers.get('dataset:state')?.handler({})
]
},
Expand All @@ -154,6 +157,7 @@ export default [
method: 'GET',
middlewares: [
ensureLoggedInMiddleware,
datasetAuthInjector.handler(),
controllers.get('dataset:diff')?.handler({})
]
},
Expand All @@ -162,6 +166,7 @@ export default [
method: 'GET',
middlewares: [
ensureLoggedInMiddleware,
datasetAuthInjector.handler(),
controllers.get('dataset:exists')?.handler({})
]
}
Expand All @@ -174,6 +179,8 @@ export default [
path: 'generate-fields/:dataset_id',
method: 'GET',
middlewares: [
ensureLoggedInMiddleware,
datasetAuthInjector.handler(),
controllers.get('get:all:fields')?.handler({})
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/services/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import appConfig from '../../shared/resources/appConfig'
import { fieldsByStatus } from '../controllers/dataset_diff';
type Payload = Record<string, any>;

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

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

Expand Down
7 changes: 5 additions & 2 deletions src/main/services/keycloak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ export const authenticated = async (request: any) => {
request.session.preferred_username = preferred_username;

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

export const deauthenticated = function (request: any) {
delete request?.session?.userDetails;
delete request?.session?.roles;
delete request?.session?.userId;
delete request?.session?.email_address;
Expand All @@ -44,6 +46,8 @@ export const userCreate = async (access_token: any, userRequest: any) => {
const payload = {
email: email_address,
username: user_name,
firstName: userRequest?.first_name,
lastName: userRequest?.last_name,
enabled: true,
credentials: [
{
Expand All @@ -63,7 +67,6 @@ export const userCreate = async (access_token: any, userRequest: any) => {
.then((response) => {
const location = _.get(response, 'headers.location');
const userId = location ? _.last(location.split('/')) : null;
console.log('keyuser', userId);
if (!userId) {
throw new Error('UserId not found');
}
Expand Down
17 changes: 15 additions & 2 deletions src/main/services/keycloakAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,21 @@ export class KeycloakAuthProvider implements BaseAuthProvider {
return this.keycloak.middleware();
}

authenticate(): (req: Request, res: Response, next: NextFunction) => void {
return this.keycloak.protect();
authenticate(): (req: any, res: Response, next: NextFunction) => void {
const protect = this.keycloak.protect();

return async (req: any, res: Response, next: NextFunction) => {
protect(req, res, async () => {
try {
if (req.kauth?.grant) {
await authenticated(req);
}
next();
} catch (error) {
next(error);
}
});
};
}

async logout(req: Request, res: Response): Promise<void> {
Expand Down
103 changes: 85 additions & 18 deletions web-console-v2/src/pages/UserManagement/AddUser.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, MenuItem, Select, InputLabel, FormControl, Button, SelectChangeEvent } from '@mui/material';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, MenuItem, Select, InputLabel, FormControl, Button, SelectChangeEvent, InputAdornment, IconButton } from '@mui/material';
import { UserRequest } from './UserManagement';
import { useUserList } from 'services/user';
import { User } from './UserManagement';
import { useAlert } from 'contexts/AlertContextProvider';
import Alert from '@mui/material/Alert';
import { Visibility, VisibilityOff } from '@mui/icons-material';

interface AddUserProps {
open: boolean;
Expand Down Expand Up @@ -37,12 +38,20 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
const { data: users } = useUserList();
const { showAlert } = useAlert();
const [error, setError] = useState<boolean | null>(null);
const [passwordRequirements, setPasswordRequirements] = useState({
length: false,
uppercase: false,
lowercase: false,
number: false,
specialChar: false,
});
const [showPassword, setShowPassword] = useState<boolean>(false);

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

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setNewUser({
...newUser,
[name]: value,
});

setNewUser(prev => ({
...prev,
[name]: name === 'user_name' ? value.toLowerCase() : value
}));

if (name === 'password') {
validatePassword(value);
}
};

const validatePassword = (password: string) => {
const updatedRequirements = {
length: password.length >= 8 && password.length <= 15,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /\d/.test(password),
specialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password),
};

setPasswordRequirements(updatedRequirements);
};

const getPasswordHelperText = () => {
const requirements = [];

if (!passwordRequirements.length) {
requirements.push('8-15 characters');
}
if (!passwordRequirements.uppercase) {
requirements.push('at least one uppercase letter');
}
if (!passwordRequirements.lowercase) {
requirements.push('at least one lowercase letter');
}
if (!passwordRequirements.number) {
requirements.push('at least one number');
}
if (!passwordRequirements.specialChar) {
requirements.push('at least one special character');
}

if (requirements.length > 0) {
return `Password must contain: ${requirements.join(', ')}`;
} else {
return '';
}
};

const handleRoleChange = (e: SelectChangeEvent<string | string[]>) => {
Expand All @@ -74,15 +126,14 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
const handleSubmit = () => {
onSubmit(newUser)
.then(() => {
onClose();
resetForm();
onClose();
resetForm();
})
.catch(() => {
showAlert('Failed to create user', 'error');
setError(true);
});
};


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

const isUserNameValid = newUser?.user_name && newUser?.user_name.length >= 3;
const isEmailValid = newUser?.email_address ? emailRegex.test(newUser?.email_address) : true;
const isFirstNameValid = !newUser.first_name || newUser.first_name.length >= 3;
const isLastNameValid = !newUser.last_name || newUser.last_name.length >= 3;
const isFirstNameValid = !newUser?.first_name || newUser?.first_name.length >= 3;
const isLastNameValid = !newUser?.last_name || newUser?.last_name.length >= 3;
const isPasswordValid = newUser?.password && getPasswordHelperText() === '';
const isFormValid =
newUser?.user_name &&
newUser?.email_address &&
newUser?.password &&
isPasswordValid &&
newUser?.roles.length > 0 &&
isUsernameTaken === false &&
isEmailValid &&
isFirstNameValid &&
isLastNameValid;
isLastNameValid &&
isUserNameValid;

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

Expand Down Expand Up @@ -137,8 +190,8 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
onChange={handleChange}
required
margin="normal"
error={isUsernameTaken === true}
helperText={isUsernameTaken ? 'Username already exists' : ''}
error={isUsernameTaken === true || isUserNameValid === false}
helperText={isUsernameTaken ? 'Username already exists' : (isUserNameValid === false) ? 'Username must be at least 3 characters' : ''}
/>
<TextField
label="First Name"
Expand All @@ -148,7 +201,7 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
value={newUser.first_name}
onChange={handleChange}
margin="normal"
error={ !isFirstNameValid}
error={!isFirstNameValid}
helperText={!isFirstNameValid ? 'If provided, first name must be at least 3 characters' : ''}
/>
<TextField
Expand Down Expand Up @@ -178,14 +231,28 @@ const AddUser: React.FC<AddUserProps> = ({ open, onClose, onSubmit, currentUser
<TextField
label="Password"
name="password"
type="password"
type={showPassword ? 'text' : 'password'}
fullWidth
variant="outlined"
value={newUser.password}
onChange={handleChange}
required
margin="normal"
error={isPasswordValid === false}
helperText={isPasswordValid === false && getPasswordHelperText()}
autoComplete="new-password"
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowPassword(!showPassword)}
edge="end"
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
/>
<FormControl fullWidth margin="normal">
<InputLabel>Role</InputLabel>
Expand Down

0 comments on commit bdca0f8

Please sign in to comment.