If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:
>> LEARN SPRING SECURITYMocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
Note that this content is outdated and using the legacy OAuth stack. Take a look at Spring Securityβs latest OAuth support.
1. Overview
In this quick tutorial, weβll focus on setting up OpenID Connect with a Spring Security OAuth2 implementation.
OpenID Connect is a simple identity layer built on top of the OAuth 2.0 protocol.
And, more specifically, weβll learn how to authenticate users using the OpenID Connect implementation from Google.
2. Maven Configuration
First, we need to add the following dependencies to our Spring Boot application:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
3. The Id Token
Before we dive into the implementation details, letβs have a quick look at how OpenID works, and how weβll interact with it.
At this point, itβs, of course, important to already have an understanding of OAuth2, since OpenID is built on top of OAuth.
First, in order to use the identity functionality, weβll make use of a new OAuth2 scope called openid. This will result in an extra field in our Access Token β βid_tokenβ.
The id_token is a JWT (JSON Web Token) that contains identity information about the user, signed by the identity provider (in our case Google).
Finally, both server(Authorization Code) and implicit flows are the most commonly used ways of obtaining id_token, in our example, we will use server flow.
3. OAuth2 Client Configuration
Next, letβs configure our OAuth2 client β as follows:
@Configuration
@EnableOAuth2Client
public class GoogleOpenIdConnectConfig {
@Value("${google.clientId}")
private String clientId;
@Value("${google.clientSecret}")
private String clientSecret;
@Value("${google.accessTokenUri}")
private String accessTokenUri;
@Value("${google.userAuthorizationUri}")
private String userAuthorizationUri;
@Value("${google.redirectUri}")
private String redirectUri;
@Bean
public OAuth2ProtectedResourceDetails googleOpenId() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setClientId(clientId);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setScope(Arrays.asList("openid", "email"));
details.setPreEstablishedRedirectUri(redirectUri);
details.setUseCurrentUri(false);
return details;
}
@Bean
public OAuth2RestTemplate googleOpenIdTemplate(OAuth2ClientContext clientContext) {
return new OAuth2RestTemplate(googleOpenId(), clientContext);
}
}
And here is application.properties:
google.clientId=<your app clientId>
google.clientSecret=<your app clientSecret>
google.accessTokenUri=https://www.googleapis.com/oauth2/v3/token
google.userAuthorizationUri=https://accounts.google.com/o/oauth2/auth
google.redirectUri=http://localhost:8081/google-login
Note that:
- You first need to obtain OAuth 2.0 credentials for your Google web app from Google Developers Console.
- We used scope openid to obtain id_token.
- we also used an extra scope email to include user email in id_token identity information.
- The redirect URI http://localhost:8081/google-login is the same one used in our Google web app.
4. Custom OpenID Connect Filter
Now, we need to create our own custom OpenIdConnectFilter to extract authentication from id_token β as follows:
public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {
public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(new NoopAuthenticationManager());
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
throw new BadCredentialsException("Could not obtain access token", e);
}
try {
String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
String kid = JwtHelper.headers(idToken).get("kid");
Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid));
Map<String, String> authInfo = new ObjectMapper()
.readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (InvalidTokenException e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
}
And here is our simple OpenIdConnectUserDetails:
public class OpenIdConnectUserDetails implements UserDetails {
private String userId;
private String username;
private OAuth2AccessToken token;
public OpenIdConnectUserDetails(Map<String, String> userInfo, OAuth2AccessToken token) {
this.userId = userInfo.get("sub");
this.username = userInfo.get("email");
this.token = token;
}
}
Note that:
- Spring Security JwtHelper to decode id_token.
- id_token always contains βsubβ field which is a unique identifier for the user.
- id_token will also contain βemailβ field as we added email scope to our request.
4.1. Verifying the ID Token
In the example above, we used the decodeAndVerify() method of JwtHelper to extract information from the id_token, but also to validate it.
The first step for this is verifying that it was signed with one of the certificates specified in the Google Discovery document.
These change about once per day, so weβll use a utility library called jwks-rsa to read them:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.3.0</version>
</dependency>
Letβs add the URL that contains the certificates to the application.properties file:
google.jwkUrl=https://www.googleapis.com/oauth2/v2/certs
Now we can read this property and build the RSAVerifier object:
@Value("${google.jwkUrl}")
private String jwkUrl;
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
Finally, weβll also verify the claims in the decoded id token:
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) ||
!claims.get("aud").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
The verifyClaims() method is checking that the id token was issued by Google and that itβs not expired.
You can find more information on this in the Google documentation.
5. Security Configuration
Next, letβs discuss our security configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private OAuth2RestTemplate restTemplate;
@Bean
public OpenIdConnectFilter openIdConnectFilter() {
OpenIdConnectFilter filter = new OpenIdConnectFilter("/google-login");
filter.setRestTemplate(restTemplate);
return filter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterAfter(new OAuth2ClientContextFilter(),
AbstractPreAuthenticatedProcessingFilter.class)
.addFilterAfter(OpenIdConnectFilter(),
OAuth2ClientContextFilter.class)
.httpBasic()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/google-login"))
.and()
.authorizeRequests()
.anyRequest().authenticated();
return http.build();
}
}
Note that:
- We added our custom OpenIdConnectFilter after OAuth2ClientContextFilter
- We used a simple security configuration to redirect users to β/google-loginβ to get authenticated by Google
6. User Controller
Next, here is a simple controller to test our app:
@Controller
public class HomeController {
@RequestMapping("/")
@ResponseBody
public String home() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return "Welcome, " + username;
}
}
Sample response (after redirect to Google to approve app authorities) :
Welcome, [email protected]
7. Sample OpenID Connect Process
Finally, letβs take a look at a sample OpenID Connect authentication process.
First, weβre going to send an Authentication Request:
https://accounts.google.com/o/oauth2/auth?
client_id=sampleClientID
response_type=code&
scope=openid%20email&
redirect_uri=http://localhost:8081/google-login&
state=abc
The response (after user approval) is a redirect to:
http://localhost:8081/google-login?state=abc&code=xyz
Next, weβre going to exchange the code for an Access Token and id_token:
POST https://www.googleapis.com/oauth2/v3/token
code=xyz&
client_id= sampleClientID&
client_secret= sampleClientSecret&
redirect_uri=http://localhost:8081/google-login&
grant_type=authorization_code
Hereβs a sample Response:
{
"access_token": "SampleAccessToken",
"id_token": "SampleIdToken",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "SampleRefreshToken"
}
Finally, hereβs what the information of the actual id_token looks like:
{
"iss":"accounts.google.com",
"at_hash":"AccessTokenHash",
"sub":"12345678",
"email_verified":true,
"email":"[email protected]",
...
}
So you can immediately see just how useful the user information inside the token is for providing identity information to our own application.
8. Conclusion
In this quick intro tutorial, we learned how to authenticate users using the OpenID Connect implementation from Google.
