Skip to main content

Improving the access-control of a JHipster application

 

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

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 anyone
  • authenticated allows only authenticated users
  • hasAuthority 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!

Post by Theo LEBRUN
January 24, 2019

Comments