After writing custom code to handle signing up users, sending emails, supporting multi-factor authentication, I looked more into alternatives. Why? As a startup you have to be scrappy. Use what’s free, create solutions for platform parts that cost too much for where you’re at. Eventually, you will grow out of this and will need to look at alternatives. This isn’t just for scale, in fact I’d argue to say it’s mostly for dollar focus. Engineers are one of the largest costs you will have in your company. If you’re working on a competing product to Cognito, Auth0, Okta, etc, then go right ahead spending your engineering dollars here. If not, you will want to periodically take a look at where your dollars are going and see whether it’s worth while to purchase a solution instead.
For Cognito, costs are straight forward, though it does get pricier with the addition of advanced features. There is also a free tier which may work well depending on your business. Integration with Spring and React are relatively straight forward which will be the main point of this post.
With the right magic, integrating Cognito into your Spring application requires very minimal changes in your code. You need to do three things to get Cognito to play well with Spring Security.
1. Add the URL to verify JWT tokens to your application.properties
# URI used to verify JWT tokens, provided from AWS Cognito, ends in '/.well-known/jwks.json' # On SpringBoot 2.2.8 both of these entries are needed, still not sure exactly why though... spring.security.oauth2.resourceserver.jwt.jwk-set-uri=<URI> security.oauth2.resource.jwk.key-set-uri=<URI>
2. Mark your service as a resource server
@Configuration @EnableResourceServer public class MySuperAwesomeResourceServerConfig extends ResourceServerConfigurerAdapter { ...
3. Convert Cognito JWT token security roles into Spring Security roles
@Override public void configure(HttpSecurity http) throws Exception { http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(new CognitoAccessTokenConverter()); ...
@Component public class CognitoAccessTokenConverter extends JwtAuthenticationConverter { private static final String COGNITO_GROUPS = "cognito:groups"; public CognitoAccessTokenConverter() { setJwtGrantedAuthoritiesConverter(new CognitoGrantedAuthoritiesConverter()); } private static class CognitoGrantedAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> { @Override public Collection<GrantedAuthority> convert(Jwt jwt) { // You can use the AWS Console to create Cognito groups and associate users. JSONArray groups = jwt.getClaim(COGNITO_GROUPS); Object groupsAsString = groups.toArray(); // This converter requires the groups to be Spring roles, like ROLE_USER or ROLE_ADMIN. Collection<GrantedAuthority> result = Arrays.stream(groups.toArray()).map(a -> new SimpleGrantedAuthority((String)a)).collect(Collectors.toList()); return result; } } }
To integrate with your web client, you have a few options. Cognito supports a hosted UI, using pre-built components or manually integrating using the available APIs. For this article, we are going to focus on the manual integration so we can keep our existing custom UI.
Cognito provides a lot of APIs and we’re only going to cover a few to support registering, verifying and logging in a user using our sample React application. For more advanced features, like turning on MFA or doing password resets, we’ll cover in future posts.
First, let’s migrate our user registration flow to support Cognito. The main modification you will see compared to what we had before is that we are now calling the Auth API, provided by AWS Amplify (https://aws.amazon.com/amplify/), rather than our own endpoint with a slightly modified payload.
async signUp() { try { let username = this.state.username; let password = this.state.password; let email = this.state.email; const { user } = await Auth.signUp({ username, password, attributes: { email } }); } catch (error) { console.log('error signing up:', error); } }
After a user has signed up they will receive an email with a verification code, if enabled for the AWS Cognito User Pool, to confirm their account registration. Again, this is a small modification that removes the dependency on our custom endpoint we wrote in a previous post.
async confirmSignUp(username, verificationCode) { try { let response = await Auth.confirmSignUp(this.state.username, this.state.verificationCode); } catch (error) { console.log(error); } }
To support logging in, the changes are fairly similar.
Auth.signIn(this.state.username, this.state.password).then(user => { // Upon successful login you'll be able to access the id and access tokens // that can be used to call your services, such as the Spring resource server // we configured above. console.log(user.getSignInUserSession().getIdToken()); console.log(user.getSignInUserSession().getAccessToken()); });
In upcoming posts we’ll get into MFA and verification emails using Simple Email Service so we can retire the remainder of our custom code and focus on what’s next in the evolution of our system.
This code has not yet been integrated into the example project at https://github.com/joutwate/99milestoempty.
Also, if you are looking for hosting please consider Dreamhost. If you’d like to sign up with them and feel inclined to throw me some credit, please use my referral link https://www.dreamhost.com/r.cgi?571777.