By default, a JHipster application will give any authenticated user the right to perform the four basic functions (or CRUD) on every entity. This behavior is very useful to showcase the application since any user can for example create an entity and update it. However, for a real application in production that kind of behavior is not really appropriate. The application's routes and API need to be restricted so a regular user does not have total freedom on every entity.
In this blog post, I will explain how to improve the access-control of the entities generated by JHipster. That covers the API for the backend and the frontend's routes/UI for both Angular/React. These kinds of improvements can be easily achieved and I recommend doing it as soon as possible to avoid having issues when going to production.
Backend customization
Spring Security and JHipster authorities
JHipster uses Spring Security to secure the application, it is a very mature and robust framework that can be used to secure Spring-based applications. Spring Security can be easily customized to change the application's authentication and access-control to fulfill any desired requirements. JHipster uses 4 kind of users (system, anonymousUser, user and admin) that have one or multiple authorities (ROLE_ANONYMOUS, ROLE_USER and ROLE_ADMIN). More information about the type of users and authorities can be found on the JHipster security page.
User's access-control can be configured using authorities at two different levels:
- URL
- user can use the entities API
- only admin can use the management API
- HTTP verb
- user can get a user using the verb
GET
- only admin can remove a user using the verb
DELETE
- user can get a user using the verb
Thereby, the application's API access-control can be configured at the URL level or more specifically at the HTTP verb level. That gives us some nice flexibility and allows us to configure any access-control policy.
Customizing URL with patterns
The method configure
in SecurityConfiguration.java
configures the security and the part after authorizeRequests()
is used to control the URLS.
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/activate").permitAll()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/account/reset-password/init").permitAll()
.antMatchers("/api/account/reset-password/finish").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
As seen above, the default behavior of a JHipster application chains all the rules using the antMatchers
method. The pattern value **
simply means that it will match any request so "/api/**"
matches the application's API.
More details about the methods used after the URL matcher:
permitAll
allows anyoneauthenticated
allows only authenticated usershasAuthority
allows only authenticated users with a given authority
So for example, having .antMatchers("/api/**").hasAuthority(AuthoritiesConstants.ADMIN)
will only allow admin to use the application's API.
Customizing an endpoint for a specific HTTP verb
In some cases, the access-control will be based on the HTTP verb and it can't be done using the URL patterns. In this case, the configuration must be done at the method level in the resource.
The code below from the class UserResource.java
shows how JHipster does that:
@GetMapping("/users/{login:" + Constants.LOGIN_REGEX + "}")
@Timed
public ResponseEntity<UserDTO> getUser(@PathVariable String login) {
log.debug("REST request to get User : {}", login);
return ResponseUtil.wrapOrNotFound(
userService.getUserWithAuthoritiesByLogin(login)
.map(UserDTO::new));
}
@DeleteMapping("/users/{login:" + Constants.LOGIN_REGEX + "}")
@Timed
@PreAuthorize("hasRole(\"" + AuthoritiesConstants.ADMIN + "\")")
public ResponseEntity<Void> deleteUser(@PathVariable String login) {
log.debug("REST request to delete User: {}", login);
userService.deleteUser(login);
return ResponseEntity.ok().headers(HeaderUtil.createAlert( "userManagement.deleted", login)).build();
}
Both methods use the URL /api/users/{login}
but with a different HTTP verb (GET
and DELETE
). The annotation @PreAuthorize
makes sure that the user can invoke the method. In the example above, the call to delete a user will fail if the authenticated user is not an admin.
The annotation @PreAuthorize
can be used for very granular access-control and solve the problem mentioned in the introduction.
Entities owned by a user
One last concept about the backend that might be useful to implement is to have access-control at the user level. That way a regular user will only have access to its entities and will not be allowed to view other users entities. That can be done at the UI level but that will not protect a malicious user from brute forcing the API with a range of IDs for example.
In order to implement this logic, a relationship must be created between the entities and the User
entity. Then using the class SecurityUtils
, the login of the connected user can be retrieved.
Here is an example of this business implementation using the entity BankAccount
that has a ManyToOne
relationship with User
:
@GetMapping("/bank-accounts/{id}")
@Timed
public ResponseEntity<BankAccountDTO> getBankAccount(@PathVariable Long id) {
log.debug("REST request to get BankAccount : {}", id);
Optional<BankAccountDTO> bankAccountDTO = bankAccountRepository.findById(id)
.map(bankAccountMapper::toDto);
// Return 404 if the entity is not owned by the connected user
Optional<String> userLogin = SecurityUtils.getCurrentUserLogin();
if (bankAccountDTO.isPresent() &&
userLogin.isPresent() &&
userLogin.get().equals(bankAccountDTO.get().getUserLogin())) {
return ResponseUtil.wrapOrNotFound(bankAccountDTO);
} else {
return ResponseEntity.notFound().build();
}
}
With the above code, the user will only be able to retrieve their own bank account and will get a 404 response if they try another user's bank account.
Frontend customization
Securing the backend should be the first thing to do but the frontend should be changed as well to avoid having a bad user experience. Routes
manage the access-control and they use the same authorities as the backend which makes things pretty easy to implement. In addition to the routes, some elements of the UI can be hidden for the regular user.
I will explain how to limit the access by changing the routes and the UI for both React and Angular. The JHipster sample app will be used since it already contains entities. Here is the repository link for React and Angular.
React
Managing the routes in React is very easy since they are all managed in the file routes.tsx
:
const Routes = () => (
<div className="view-routes">
<ErrorBoundaryRoute path="/login" component={Login} />
<Switch>
<ErrorBoundaryRoute path="/logout" component={Logout} />
<ErrorBoundaryRoute path="/register" component={Register} />
<ErrorBoundaryRoute path="/activate/:key?" component={Activate} />
<ErrorBoundaryRoute path="/reset/request" component={PasswordResetInit} />
<ErrorBoundaryRoute path="/reset/finish/:key?" component={PasswordResetFinish} />
<PrivateRoute path="/admin" component={Admin} hasAnyAuthorities={[AUTHORITIES.ADMIN]} />
<PrivateRoute path="/account" component={Account} hasAnyAuthorities={[AUTHORITIES.ADMIN, AUTHORITIES.USER]} />
<PrivateRoute path="/entity" component={Entities} hasAnyAuthorities={[AUTHORITIES.USER]} />
<ErrorBoundaryRoute path="/" component={Home} />
</Switch>
</div>
);
The attribute hasAnyAuthorities
defines which authorities are required to let the user use the corresponding route.
In order to hide UI elements for a non-admin user, a new property isAdmin
can be used like below:
render() {
const { bankAccountList, match, isAdmin } = this.props;
return (
<div>
[...]
{isAdmin && <Button tag={Link} to={`${match.url}/${bankAccount.id}/delete`} color="danger" size="sm">
<FontAwesomeIcon icon="trash" />{' '}
<span className="d-none d-md-inline">
<Translate contentKey="entity.action.delete">Delete</Translate>
</span>
</Button>}
[...]
</div>
);
}
const mapStateToProps = ({ bankAccount, authentication }: IRootState) => ({
bankAccountList: bankAccount.entities,
isAdmin: hasAnyAuthority(authentication.account.authorities, [AUTHORITIES.ADMIN])
});
Angular
In Angular, each module has a file called modulename.route.ts
that contains the routes definition. Like React, an array of authorities can be used in order to control what the user can do.
Here is the route that deletes a BankAccount
in bank-account.route.ts
:
export const bankAccountPopupRoute: Routes = [
{
path: 'bank-account/:id/delete',
component: BankAccountDeletePopupComponent,
resolve: {
bankAccount: BankAccountResolve
},
data: {
authorities: ['ROLE_USER'],
pageTitle: 'jhipsterSampleApplicationApp.bankAccount.home.title'
},
canActivate: [UserRouteAccessService],
outlet: 'popup'
}
];
Replacing the authority ROLE_USER
by ROLE_ADMIN
will remove the right of deleting a BankAccount
for a regular user.
In order to display the delete
button only for admins, the attribute *jhiHasAnyAuthority="'ROLE_ADMIN'"
can be used like below:
<button *jhiHasAnyAuthority="'ROLE_ADMIN'"
type="submit"
[routerLink]="['/', { outlets: { popup: 'bank-account/'+ bankAccount.id + '/delete'} }]"
replaceUrl="true"
queryParamsHandling="merge"
class="btn btn-danger btn-sm">
<fa-icon [icon]="'times'"></fa-icon>
<span class="d-none d-md-inline" jhiTranslate="entity.action.delete">Delete</span>
</button>
Conclusion
Improving the access-control of the API generated by JHipster is pretty easy to achieve, thanks to Spring Security. It can be easily extended and custom authorities can be added to fulfill any specific requirement.
Since the authorities are shared with the UI, the same logic can be done with the frontend's routes to avoid having a regular user access unauthorized content. And finally with the help of prebuilt JHipster functions, page elements such as link/button can be hidden for regular users and shown for admins.
One last thing if the application uses a JWT, always change the Base64 token in application-prod.yml
before going to production!