IntroductionIn the previous article ( Authentication Service Part II), we began implementing the Authentication Service. In this article, we finish our discussion by covering selected methods within the AuthenticationService class, AuthorizationFilter, and AuthenticationController REST endpoint.
RequirementsBefore you get started, you will need the following:
Building the Authentication Service
Authentication ServiceThe AuthenticationService class contains the business logic of the service. Due to the size of the class we will not reproduce it here in its entirety. Instead, we will cover the main highlights of the class.
@Autowired fieldsWhile the AuthenticationClass provides the business logic, it depends on the framework to create and @Autowired the following fields:
RegistrationThe registration method is responsible for creating new application accounts.
The method starts by checking the registration rules, throwing RegistrationExceptions with the appropriate I18N message key when necessary. When all the registration conditionals have passed, we create a user entity, publish an AccountRegisteredEvent, and send a registration email to the user.
AuthenticationThe authentication method is responsible for verifying a valid credential set (email/password) has been registered with the application and generates a JSON Web Token ( JWT) that corresponds to these credentials.
The method starts with an option test for required services. This test ensures that all the declared services are available before authenticating. Due to the application's distributed nature, we may want to all users to authenticate only when set of services are available.
We then attempt to load the user by the supplied username (in this case, email address). If the user is not found, we publish a failing CredentialAuthenticationRequestEvent to log the attempt, and throw an AuthenticationException.
If the user is found, its activeStatus is checked. The activeStatus that controls whether an existing user is allowed to authenticate. If the activeStatus is false, we publish a failing CredentialAuthenticationRequestEvent to log the attempt, and throw an AuthenticationException indicating that the account has been disabled.
If the activeStatus is true, we compare the user's stored password with the authentication password. If the password doesn't match, we throw a new AuthenticationException with the invalid credentials message. If the password matches we generate the JWT, publish a passing CredentialAuthenticationRequestEvent to log the success.
Change PasswordThe changePassword method provides a mechanism for authenticated users to change their existing password.
It starts by looking up the User using the accountId. This value is present in the JWT. The method requires the user supply the current password along with the a new password and a confirmation password. We require the current password be supplied to prevent a third-party from changing the password if they gain control of an authenticated client. The method then checks that the new password meets the appropriate criteria raising a ChangePasswordException with the corresponding I18N message key if it doesn't. If all criteria are met, the new password is encrypted and stored in the user repository and a PasswordChangedEvent is published.
Password Recovery RequestThe recoverPassword method provides a mechanism for users to change forgotten passwords. This is accomplished by generating a recovery password and emailing it to the users.
The method starts by looking up the user by their email address. If no user is found, the method throws a RecoverPasswordException. If a corresponding user is found, the method will generate a Recovery Code (we use a random UUID) and an expiration time that are store in the User entity. The method then sends an email message containing the Recovery Code to the user's email address. We finish by publishing a PasswordRecoveryRequestedEvent.
Password Recovery ResetThe resetPassword method operates in a similar fashion to the changePassword method with two exceptions.
First, we confirm that the recoveryCode matches the value store in the user entity. If not, the method throws a ResetPasswordException with the invalid code message key.
Secondly, we confirm that the current time is prior to the recovery code expiration time. If not, the method throws a ResetPasswordException with the recovery code expired message.
If all the method conditions are met, the new password will be encrypted and stored, an a PasswordRecoveryCompletedEvent will be published.
Token RefreshThe refreshToken method performs a function similar to the authenticate method. However, instead of requiring a credential set, the method requires a refreshToken.
Like the authenticate method, the refreshToken also contains a test for a required set of active services that can be optionally enabled. However, this method performs it's lookup by a refreshToken value. The method checks that the User's status is active before generating a new token. This step is important to prevent previously authenticated users from continuously refreshing their token and using the application in the event that their account has been disabled. Finally, once we have run the gauntlet of conditionals we can generate a new token.
Metric CollectionWe will collect five service-specific metrics:
- registration.created-a counter collecting the number of new registrations.
- authentication.successul-a counter collecting the number of successful authentication attempts.
- authentication.failed-a counter collecting the number of failed authentication attempts.
- password.changed-a counter collecting the number of password change requests.
- password.recovered-a counter collecting the number of password change requests.
AuthorizationFilterSeveral of the features we have discuss require that the caller be authenticated and authorized. As we mentioned in the previous article, this is handled by passing the JWT in the HTTP Authorization Header. Rather than require each service method to test for the presence and validity of the JWT, we centralize this function in the JWTAuthorizationFilter. The filter is called prior to invoking the service's controller which allows the service to inspect the service's request path and authorization header. With the path and authorization header we can query the JWTRoleTable for required roles and verify if the request's JWT contains the requisite roles.
The paths and required roles are stored in the class's uriRoleTable. This provides the mapping between a path and an array of required roles.
The bulk of the filter resided in the doFilter method which extends the standard javax.servlet.Filter. We start by getting the list of role required to process the request URI. If the list is empty, we dont need to check the JWT roles and we continue to the controller. If the list is not empty, we extract attempts to extract the JWT from the HTTP request. We then perform the following tests:
- If no JWT is present, the filter sends an HTTP response containing the 401 UNUATHORIZED status code and the Token Missing message.
- If no JWT has expired, the filter sends an HTTP response containing the 401 UNUATHORIZED status code and the Token Expired message.
- If no JWT does not contain all the required roles, the filter sends an HTTP response containing the 401 UNUATHORIZED status code and the Token Insufficient Privileges message.
Now that we have an authorization filter, we need to configure our service to use the filter. To do this we create the FilterConfig class.
In this class we add the @Configuration annotation to notify the framework that this is a configuration class. We then create a FilterRegistrationBean that maps all request paths to the JWTAuthorizationFilter (the URL_PATTERN="/*" accomplishes this).
Authentication ControllerThe AuthenticationController provides our service's REST endpoint adapter.
The AuthenticationController exposes nine REST endpoint methods. Each of these methods handle the translation of HTTP requests to the corresponding AuthenticationService class methods and process the results back into HTTP responses handling exceptions, and I18N translations.
From a security perspective, we can divide them into three groups::
UnsecuredThese methods dont require authentication/authorization.
User-Role securedThese methods require that the JWT contain the User role.
Admin-Role securedThese methods require that the JWT contain the Admin role. These methods are provides to allow basic user adminstration functions.
- AuthenticationService Github repository
- AuthenticationService Docker hub image.
- ThinkMicroservice AuthenticationService Dashboard.