1. Introduction

By default, JHipster applications use a local data store to hold usernames and passwords. In many real-world scenarios, however, it might be desirable to use an existing external service for authentication.

In this tutorial, we’ll look at how to use an external service for authentication in JHipster. This could be any well-known service such as LDAP, social login, or any arbitrary service that accepts a username and password.

2. Authentication in JHipster

JHipster uses Spring Security for authentication. The AuthenticationManager class is responsible for validating username and passwords.

The default AuthenticationManager in JHipster simply checks the username and password against a local data store. This could be MySQL, PostgreSQL, MongoDB, or any of the alternatives that JHipster supports.

It’s important to note that the AuthenticationManager is only used for initial login. Once a user has authenticated, they receive a JSON Web Token (JWT) that is used for subsequent API calls.

2.1. Changing Authentication in JHipster

But what if we already have a data store that contains usernames and passwords, or a service that performs authentication for us?

To provide a custom authentication scheme, we simply create a new bean of type AuthenticationManager. This will take precedence over the default implementation.

Below is an example that shows how to create a custom AuthenticationManager. It only has one method to implement:

public class CustomAuthenticationManager implements AuthenticationManager {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        try {
            ResponseEntity<LoginResponse> response =
                restTemplate.postForEntity(REMOTE_LOGIN_URL, loginRequest, LoginResponse.class);
            
            if(response.getStatusCode().is2xxSuccessful()) {
                String login = authentication.getPrincipal().toString();
                User user = userService.getUserWithAuthoritiesByLogin(login)
                  .orElseGet(() -> userService.createUser(
                    createUserDTO(response.getBody(), authentication)));
                return createAuthentication(authentication, user);
            }
            else {
                throw new BadCredentialsException("Invalid username or password");
            }
        }
        catch (Exception e) {
            throw new AuthenticationServiceException("Failed to login", e);
        }
    }
}

In this example, we pass the username and credentials from the Authentication object to an external API.

If the call succeeds, we return a new UsernamePasswordAuthenticationToken to indicate success. Note that we also create a local user entry, which we’ll discuss later on.

If the call fails, we throw some variant of AuthenticationException so that Spring Security will gracefully fallback for us.

This example is intentionally simple to show the basics of custom authentication. However, it could perform more complex operations such as LDAP binding and authentication or use OAuth.

3. Other Considerations

Up until now, we’ve focused on the authentication flow in JHipster. But there are several other areas of our JHipster application we have to modify.

3.1. Front-End Code

The default JHipster code implements the following user registration and activation process:

  • A user signs up for an account using their email and other required details
  • JHipster creates an account and sets it as inactive and then sends an email to the new user with an activation link
  • Upon clicking the link, the user’s account is marked as active

There is a similar flow for password reset as well.

These all make sense when JHipster is managing user accounts. But they are not required when we’re relying on an external service for authentication.

Therefore, we need to take steps to ensure these account management features are not accessible to the user.

This means removing them from the Angular or React code, depending on which framework is being used in the JHipster application.

Using Angular as an example, the default login prompt includes links to password reset and registration. We should remove them from app/shared/login/login.component.html:

<div class="alert alert-warning">
  <a class="alert-link" (click)="requestResetPassword()">Did you forget your password?</a>
</div>
<div class="alert alert-warning">
  <span>You don't have an account yet?</span>
   <a class="alert-link" (click)="register()">Register a new account</a>
</div>

We must also remove the unneeded navigation menu items from app/layouts/navbar/navbar.component.html:

<li *ngSwitchCase="true">
  <a class="dropdown-item" routerLink="password" routerLinkActive="active" (click)="collapseNavbar()">
    <fa-icon icon="clock" fixedWidth="true"></fa-icon>
    <span>Password</span>
  </a>
</li>

and

<li *ngSwitchCase="false">
  <a class="dropdown-item" routerLink="register" routerLinkActive="active" (click)="collapseNavbar()">
    <fa-icon icon="user-plus" fixedWidth="true"></fa-icon>
    <span>Register</span>
  </a>
</li>

Even though we removed all the links, a user could still manually navigate to these pages. The final step is to remove the unused Angular routes from app/account/account.route.ts.

After doing this, only the settings route should remain:

import { settingsRoute } from './';
const ACCOUNT_ROUTES = [settingsRoute];

3.2. Java APIs

In most cases, simply removing the front-end account management code should be sufficient. However, to be absolutely sure the account management code is not invoked, we can also lock down the associated Java APIs.

The quickest way to do this is to update the SecurityConfiguration class to deny all requests to the associated URLs:

.antMatchers("/api/register").denyAll()
.antMatchers("/api/activate").denyAll()
.antMatchers("/api/account/reset-password/init").denyAll()
.antMatchers("/api/account/reset-password/finish").denyAll()

This will prevent any remote access to the APIs, without having to remove any of the code.

3.3. Email Templates

JHipster applications come with a set of default email templates for account registration, activation, and password resets. The previous steps will effectively prevent the default emails from being sent, but in some cases, we might want to reuse them.

For example, we might want to send a welcome email when a user logs in for the first time. The default template includes steps for account activation, so we have to modify it.

All of the email templates are located in resources/templates/mail. They are HTML files that use Thymeleaf to pass data from Java code into the emails.

All we have to do is to edit the template to include the desired text and layout and then use the MailService to send it.

3.4. Roles

When we create the local JHipster user entry, we also have to take care to ensure it has at least one role. Normally, the default USER role is sufficient for new accounts.

If the external service provides its own role mapping, we have two additional steps:

  1. Ensure any custom roles exist in JHipster
  2. Update our custom AuthenticationManager to set the custom roles when creating new users

JHipster also provides a management interface for adding and removing roles to users.

3.5. Account Removal

It’s worth mentioning that JHipster also provides an account removal management view and API. This view is only available to administrator users.

We could remove and restrict this code as we did for account registration and password reset, but it’s not really necessary. Our custom AuthenticationManager will always create a new account entry when someone logs in, so deleting the account doesn’t actually do much.

4. Conclusion

In this tutorial, we’ve seen how to replace the default JHipster authentication code with our own authentication scheme. This could be LDAP, OIDC, or any other service that accepts a username and password.

We’ve also seen that using an external authentication service also requires some changes to other areas of our JHipster application. This includes front end views, APIs, and more.

As always, the example code from this tutorial is available over on GitHub.