Authenticate Spring boot API with AWS Cognito

Kavya Babu
The Startup
Published in
6 min readApr 19, 2020

When a requirement came to implement identity management in an existing web-app, I had two options in front of me. One, to go with an authentication microservice of our own with database etc managed by us; or two, to use a serverless Identity management tool. Since this app which we were developing was a support tool intended to be used only during the initial phase of the project just by the internal team, having a pluggable identity management solution sounded reasonable compared to putting effort to develop our authentication service and then maintaining it. Hence we decided to go with the serverless option. AWS offers a cool identity management tool called AWS Cognito.

AWS Cognito provides user management, authentication and authorization for the apps. Hence we needn’t worry about the authentication/user data storage and access key generation logic. Using Cognito, users will be able to sign in with their user name and password or through any supported third party oAuth 2 provider such as Twitter, Facebook, Google or Apple. Additionally, with Cognito Identity Pools, we can access resources on AWS through IAM roles. Since we were already on the AWS stack, we decided to go with Cognito.

Currently, we have a frontend app that is exposed to the internet which needs to access a service that is inside a VPC to fetch data. With a front-end login already in place, we needed to authorize the API using id token which basically is a JW Token issued by AWS Cognito.

Sequence diagram for AWS Cognito based authentication

This post covers the API authentication of a Spring Boot application using AWS Cognito. We will get in detail about how to authenticate the API upon receiving the JWT token frontend.

Here’s the plan!

To authenticate an API request with AWS Cognito, we need to complete two steps:

1. Verify JWT

When a request hits the app, using a filter or interceptor, get the request. Retrieve the ‘Authorisation’ header. The authorization header will be of format ‘Bearer <TOKEN>’. Retrieve the token from the header. The token here will be the id token sent from the client-side. The ID Token contains claims about the identity of the authenticated user such as name, email, and phone_number.

To Validate the JWT signature, we need to get the JSON Web Key Set from AWS Cognito. The key set can be found athttps://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json. Using nimbus-jose library, these steps will be a piece of cake.

2. Verify JWT claims

  1. Verify that the token is not expired.
  2. The audience (aud) claim should match the app client ID created in the Amazon Cognito user pool.
  3. The issuer (iss) claim should match the user pool. For example, a user pool created in the us-east-1 region will have an iss value of:
  4. https://cognito-idp.us-east-1.amazonaws.com/<userpoolID>.
  5. Check the token_use claim.
  • If you are only using the ID token, its value must be id.
  • If you are using both ID and access tokens, the token_use claim must be either id or access.

On completing steps 1 and 2, JWT token will be validated and API will be authenticated.

Implementation

First let’s add the required dependencies to pom file:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>4.23</version>
</dependency>
</dependencies>

Nimbus-jose-jwt is an open-source library for JWT creation and parsing. This library comes quite handy for JWKs. The details can be found here : https://connect2id.com/products/nimbus-jose-jwt.

We would need to setup some minimal properties for this application. Let’s do that as well in the application.yml file

com:
kb:
jwt:
aws:
userPoolId: "xx-xx-xxxxxxxxx"
region: "xx-xxxx-x"

Here, userPoolId is AWS Cognito user-pool ID and region is the corresponding user pool region.

Now we will need to define a bean for processing the JWT. It will also configure AWS Cognito keys URL as the JWKs source. Processing and verification of JWT token are done under the hood by this bean.

@Bean
public ConfigurableJWTProcessor configurableJWTProcessor() throws MalformedURLException {
ResourceRetriever resourceRetriever =
new DefaultResourceRetriever(2000,2000);
URL jwkURL= new URL("cognito-jwk-url-goes-here");
JWKSource keySource= new RemoteJWKSet(jwkURL, resourceRetriever);
ConfigurableJWTProcessor jwtProcessor= new DefaultJWTProcessor();
JWSKeySelector keySelector= new JWSVerificationKeySelector(RS256, keySource);
jwtProcessor.setJWSKeySelector(keySelector);
return jwtProcessor;
}

A filter to intercept the incoming request and verifying the JWT token is the next thing to be implemented. This filter will get the “Authorization” header and verify the bearer token in it.

@Component
public class AwsCognitoJwtAuthFilter extends GenericFilter {

private static final Log logger = LogFactory.getLog(AwsCognitoJwtAuthFilter.class);
private AwsCognitoIdTokenProcessor cognitoIdTokenProcessor;

public AwsCognitoJwtAuthFilter(AwsCognitoIdTokenProcessor cognitoIdTokenProcessor) {
this.cognitoIdTokenProcessor = cognitoIdTokenProcessor;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
Authentication authentication;
try {
authentication = this.cognitoIdTokenProcessor.authenticate ((HttpServletRequest)request);
if (authentication != null) { SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception var6) {
logger.error("Cognito ID Token processing error", var6);
SecurityContextHolder.clearContext();
}

filterChain.doFilter(request, response);
}
}

To make things neat, we will add a JWTConfiguration file with all the required configurations:

@Component
@ConfigurationProperties(
prefix = "com.kb.jwt.aws"
)
public class JwtConfiguration {
private String userPoolId;
private String identityPoolId;
private String jwkUrl;
private String region = "us-east-2";
private String userNameField = "cognito:username";
private int connectionTimeout = 2000;
private int readTimeout = 2000;
private String httpHeader = "Authorization";

public JwtConfiguration() {
}

public String getJwkUrl() {
return this.jwkUrl != null && !this.jwkUrl.isEmpty() ? this.jwkUrl : String.format("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", this.region, this.userPoolId);
}

public String getCognitoIdentityPoolUrl() {
return String.format("https://cognito-idp.%s.amazonaws.com/%s", this.region, this.userPoolId);
}
.
.
.
}

Also, we need to define a JwtAuthentication object with the claims and user details.

import com.nimbusds.jwt.JWTClaimsSet;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class JwtAuthentication extends AbstractAuthenticationToken {

private final Object principal;
private JWTClaimsSet jwtClaimsSet;

public JwtAuthentication(Object principal, JWTClaimsSet jwtClaimsSet, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.jwtClaimsSet = jwtClaimsSet;
super.setAuthenticated(true);
}
.
.
.
}

The function authenticate will verify the JWT. For my use case, I didn’t require any GrantedAuthority, hence keeping it as empty for JwtAuthentication.

@Autowired
private JwtConfiguration jwtConfiguration;

@Autowired
private ConfigurableJWTProcessor configurableJWTProcessor;
public Authentication authenticate(HttpServletRequest request) throws Exception {
String idToken = request.getHeader(this.jwtConfiguration.getHttpHeader());
if (idToken != null) {
JWTClaimsSet claims =
this.configurableJWTProcessor
.process(this.getBearerToken(idToken),null);
validateIssuer(claims);
verifyIfIdToken(claims);
String username = getUserNameFrom(claims);
if (username != null) {
User user = new User(username, "", of());
return new JwtAuthentication(user, claims, of());
}
}
return null;
}

private String getUserNameFrom(JWTClaimsSet claims) {
return claims.getClaims()
.get(this.jwtConfiguration.getUserNameField())
.toString();
}

private void verifyIfIdToken(JWTClaimsSet claims) throws Exception {
if (!claims.getIssuer().equals(this.jwtConfiguration.getCognitoIdentityPoolUrl())) {
throw new Exception("Not an ID Token");
}
}

private void validateIssuer(JWTClaimsSet claims) throws Exception {
if (!claims.getIssuer().equals(this.jwtConfiguration.getCognitoIdentityPoolUrl())) {
throw new Exception(String.format("Issuer %s does not match cognito idp %s", claims.getIssuer(), this.jwtConfiguration.getCognitoIdentityPoolUrl()));
}
}

private String getBearerToken(String token) {
return token.startsWith("Bearer ") ? token.substring("Bearer ".length()) : token;
}

ConfigurableJWTProcessor component will do all the verification by calling the AWS Cognito JWK URL, building the signature and comparing it with the incoming id token. These complex operations are done under the hood for us by nimbus-jose. How cool is that!

To authenticate all requests coming to the app, let’s add a configuration for spring security. Health check endpoints are permitted here. We will authenticate endpoints with ‘/api’ in its URL.

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private AwsCognitoJwtAuthFilter awsCognitoJwtAuthenticationFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {

http.headers().cacheControl();
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().authenticated()
.and()
.addFilterBefore(awsCognitoJwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}

}

Time to add a controller end point see things working.

@RestController
public class SimpleController {

@GetMapping(path = "/api/hello")
public String getResp(){
return "Hey authenticated request";
}

}

With these settings in place, start the application. Get the token after logging in the front-end and use it as header to hit our demo endpoint.

curl -v -H “Authorization: Bearer <Token>” -s -N http://localhost:8081/api/hello

We should be able to see get the response with status 200 OK.

Now, if we send an invalid or expired token, we will get HTTP status 403.

That’s how we authenticate our spring-boot based microservices with AWS Cognito. You can find the source code for this demo right here : https://github.com/KavyaBabu/AWSCognitoDemo.

Do post your queries and feedback in the comments section. Thank you for reading this post. Happy learning. 😊

--

--