I have setup a small project to implement OAuth2 Login with Google+ API, using Spring Boot (1.5.2), Spring Security and Spring Security OAuth2.
You can find source in: https://github.com/ccoloradoc/OAuth2Sample
I am able to authenticate with google and pull out user information. However, after I logout I cannot login again since I got a "400 Bad Request", after I attempt to connect "https://accounts.google.com/o/oauth2/auth" with my RestTemplate to invoke google api.
See Filter attemptAuthentication method for further reference.
Here is my Security configuration class
@Configuration
@EnableGlobalAuthentication
@EnableOAuth2Client
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@PropertySource(value = {"classpath:oauth.properties"})
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Resource
@Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
@Autowired
private OAuth2ClientContextFilter oAuth2ClientContextFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.
authorizeRequests()
.antMatchers(HttpMethod.GET, "/login","/public/**", "/resources/**","/resources/public/**").permitAll()
.antMatchers("/google_oauth2_login").anonymous()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.and()
.csrf().disable()
.logout()
.logoutSuccessUrl("/")
.logoutUrl("/logout")
.deleteCookies("remember-me")
.and()
.rememberMe()
.and()
.addFilterAfter(oAuth2ClientContextFilter,ExceptionTranslationFilter.class)
.addFilterAfter(googleOAuth2Filter(),OAuth2ClientContextFilter.class)
.userDetailsService(userDetailsService);
// @formatter:on
}
@Bean
@ConfigurationProperties("google.client")
public OAuth2ProtectedResourceDetails auth2ProtectedResourceDetails() {
return new AuthorizationCodeResourceDetails();
}
@Bean
public OAuth2RestTemplate oauth2RestTemplate() {
return new OAuth2RestTemplate(auth2ProtectedResourceDetails(),
new DefaultOAuth2ClientContext(accessTokenRequest));
}
@Bean
public GoogleOAuth2Filter googleOAuth2Filter() {
return new GoogleOAuth2Filter("/google_oauth2_login");
}
/*
* Building our custom Google Provider
* */
@Bean
public GoogleOauth2AuthProvider googleOauth2AuthProvider() {
return new GoogleOauth2AuthProvider();
}
/*
* Using autowired to assign it to the auth manager
* */
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(googleOauth2AuthProvider());
}
@Bean
public SpringSecurityDialect springSecurityDialect() {
return new SpringSecurityDialect();
}
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
Here is my authentication provider:
public class GoogleOauth2AuthProvider implements AuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(GoogleOauth2AuthProvider.class);
@Autowired(required = true)
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
logger.info("Provider Manager Executed");
CustomOAuth2AuthenticationToken token = (CustomOAuth2AuthenticationToken) authentication;
UserDetailsImpl registeredUser = (UserDetailsImpl) token.getPrincipal();
try {
registeredUser = (UserDetailsImpl) userDetailsService
.loadUserByUsername(registeredUser.getEmail());
} catch (UsernameNotFoundException usernameNotFoundException) {
logger.info("User trying google/login not already a registered user. Register Him !!");
}
return token;
}
@Override
public boolean supports(Class<?> authentication) {
return CustomOAuth2AuthenticationToken.class
.isAssignableFrom(authentication);
}
}
UserDetailService is an implementation from spring security core that reads user from database and translate it to a UserDetails POJO that implements spring security core UserDetails.
Here is my filter implementation:
public class GoogleOAuth2Filter extends AbstractAuthenticationProcessingFilter {
/**
* Logger
*/
private static final Logger log = LoggerFactory.getLogger(GoogleOAuth2Filter.class);
private static final Authentication dummyAuthentication;
static {
dummyAuthentication = new UsernamePasswordAuthenticationToken(
"dummyUserName23452346789", "dummyPassword54245",
CustomUserDetails.DEFAULT_ROLES);
}
private static final String NAME = "name";
private static final String EMAIL = "email";
private static final String PICTURE = "picture";
private static final Logger logger = LoggerFactory
.getLogger(GoogleOAuth2Filter.class);
@Value(value = "${google.authorization.url}")
private String googleAuhorizationUrl;
public GoogleOAuth2Filter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Autowired
private UserService userService;
@Autowired
private OAuth2RestTemplate oauth2RestTemplate;
@Autowired
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException,
IOException, ServletException {
logger.info("Google Oauth Filter Triggered!!");
URI authURI;
try {
authURI = new URI(googleAuhorizationUrl);
} catch (URISyntaxException e) {
log.error("\n\n\n\nERROR WHILE CREATING GOOGLE AUTH URL", e);
return null;
}
SecurityContext context = SecurityContextHolder.getContext();
// auth null or not authenticated.
String code = request.getParameter("code");
Map<String, String[]> parameterMap = request.getParameterMap();
logger.debug(parameterMap.toString());
if (StringUtils.isEmpty(code)) {
// Google authentication in progress. will return null.
logger.debug("Will set dummy user in context ");
context.setAuthentication(dummyAuthentication);
// trigger google oauth2.
// ERROR ON SECOND LOGIN ATTEMPT
oauth2RestTemplate.postForEntity(authURI, null, Object.class);
return null;
} else {
logger.debug("Response from Google Recieved !!");
ResponseEntity<Object> forEntity = oauth2RestTemplate.getForEntity(
"https://www.googleapis.com/plus/v1/people/me/openIdConnect",
Object.class);
@SuppressWarnings("unchecked")
Map<String, String> profile = (Map<String, String>) forEntity.getBody();
CustomOAuth2AuthenticationToken authenticationToken = getOAuth2Token(
profile.get(EMAIL), profile.get(NAME), profile.get(PICTURE));
authenticationToken.setAuthenticated(false);
return getAuthenticationManager().authenticate(authenticationToken);
}
}
private CustomOAuth2AuthenticationToken getOAuth2Token(
String email, String name, String picture) {
User user = userService.findByEmail(email);
//Register user
if(user == null) {
user = new User(name, email, picture);
userService.saveOrUpdate(user);
}
UserDetailsImpl registeredUser = new UserDetailsImpl(name, email, picture);
CustomOAuth2AuthenticationToken authenticationToken =
new CustomOAuth2AuthenticationToken(registeredUser);
return authenticationToken;
}
}
Google Sign-In manages the OAuth 2.0 flow and token lifecycle, simplifying your integration with Google APIs. A user always has the option to revoke access to an application at any time.
Spring Security OAuth2 − Implements the OAUTH2 structure to enable the Authorization Server and Resource Server. Spring Security JWT − Generates the JWT Token for Web security. Spring Boot Starter JDBC − Accesses the database to ensure the user is available or not. Spring Boot Starter Web − Writes HTTP endpoints.
Thank you Cristian, you have no idea how much your code helped to start a foundation for my own code. I modified your original OAuth2 Github project and change it to the following code.
GoogleOAuth2Filter.java
package tech.aabo.celulascontentas.oauth.filter;
import static java.lang.Math.toIntExact;
import com.google.api.client.auth.oauth2.AuthorizationCodeResponseUrl;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.auth.oauth2.TokenResponseException;
import com.google.api.client.googleapis.auth.oauth2.*;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.plus.Plus;
import com.google.api.services.plus.model.Person;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import tech.aabo.celulascontentas.oauth.domain.User;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Arrays;
import java.util.Calendar;
import java.util.UUID;
/**
* Created by colorado on 9/03/17.
* Modified by frhec on 7/06/18
*/
public class GoogleOAuth2Filter extends AbstractAuthenticationProcessingFilter {
/**
* Logger
*/
private static final Logger logger = LoggerFactory.getLogger(GoogleOAuth2Filter.class);
public GoogleOAuth2Filter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
}
@Autowired
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String CLIENT_SECRET_FILE = "client_secret.json";
SecurityContext context = SecurityContextHolder.getContext();
if(context.getAuthentication() == null) {
GoogleClientSecrets clientSecrets = loadSecret(CLIENT_SECRET_FILE);
if (StringUtils.isEmpty(request.getQueryString())) {
try {
GoogleAuthorizationCodeRequestUrl auth = new GoogleAuthorizationCodeRequestUrl(clientSecrets.getDetails().getClientId(),
request.getRequestURL().toString(), Arrays.asList(
"https://www.googleapis.com/auth/plus.login",
"https://www.googleapis.com/auth/plus.me",
"https://www.googleapis.com/auth/plus.profile.emails.read")).setState("/user");
auth.setAccessType("offline");
response.addHeader("Place","Before");
response.sendRedirect(auth.build());
} catch (IOException e) {
e.printStackTrace();
}
} else {
response.addHeader("Place","After");
AuthorizationCodeResponseUrl authResponse = new AuthorizationCodeResponseUrl(transformName(request, 0));
// check for user-denied error
if (authResponse.getError() != null) {
logger.info("Denied");
} else {
try {
assert clientSecrets != null;
Calendar calendar = Calendar.getInstance();
NetHttpTransport net = new NetHttpTransport();
JacksonFactory jackson = new JacksonFactory();
GoogleTokenResponse tokenResponse =
new GoogleAuthorizationCodeTokenRequest(net, jackson,
clientSecrets.getDetails().getClientId(), clientSecrets.getDetails().getClientSecret(),
authResponse.getCode(), transformName(request, 1))
.execute();
// Use access token to call API
GoogleCredential credential;
if (tokenResponse.getRefreshToken() == null) {
credential = new GoogleCredential();
credential.setFromTokenResponse(tokenResponse);
} else {
credential = createCredentialWithRefreshToken(net, jackson, clientSecrets, tokenResponse);
}
Plus plus =
new Plus.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential)
.setApplicationName("Google Plus Profile Info")
.build();
Person profile = plus.people().get("me").execute();
// Get profile info from ID token
GoogleIdToken idToken = tokenResponse.parseIdToken();
GoogleIdToken.Payload payload = idToken.getPayload();
User auth = new User();
auth.setAccessToken(tokenResponse.getAccessToken());
auth.setId(new BigInteger(payload.getSubject().trim())); // Use this value as a key to identify a user.
auth.setUuid(UUID.randomUUID().toString());
auth.setEmail(payload.getEmail());
auth.setVerifiedEmail(payload.getEmailVerified());
auth.setName(profile.getDisplayName());
auth.setPictureURL(profile.getImage().getUrl());
auth.setLocale(profile.getLanguage());
auth.setFamilyName(profile.getName().getFamilyName());
auth.setGivenName(profile.getName().getGivenName());
auth.setStatus(true);
auth.setExpired(false);
auth.setLocked(false);
auth.setExpiredCredentials(false);
auth.setRoles("USER");
auth.setRefreshToken(tokenResponse.getRefreshToken());
auth.setDateCreated(calendar.getTime());
calendar.add(Calendar.SECOND, toIntExact(tokenResponse.getExpiresInSeconds()));
auth.setExpirationDate(calendar.getTime());
auth.setDateModified(Calendar.getInstance().getTime());
Authentication authenticationToken = getOAuth2Token(auth);
request.authenticate(response);
if (//Validation happening) {
authenticationToken.setAuthenticated(true);
} else {
authenticationToken.setAuthenticated(false);
}
return authenticationToken;
} catch (TokenResponseException e) {
if (e.getDetails() != null) {
System.err.println("Error: " + e.getDetails().getError());
if (e.getDetails().getErrorDescription() != null) {
System.err.println(e.getDetails().getErrorDescription());
}
if (e.getDetails().getErrorUri() != null) {
System.err.println(e.getDetails().getErrorUri());
}
} else {
System.err.println(e.getMessage());
}
} catch (IOException | ServletException e) {
e.printStackTrace();
}
}
}
}else if(!context.getAuthentication().isAuthenticated()) {
setResponseUnauthenticated(response);
}else{
try {
response.sendRedirect(transformName(request,2)+"/user");
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
private void setResponseUnauthenticated(HttpServletResponse response){
try {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
//create Json Object
JSONObject values = new JSONObject();
values.put("principal", null);
values.put("authentication", null);
values.put("timestamp", String.valueOf(Timestamp.from(Instant.now())));
values.put("code",401);
values.put("message", "Not Authorized");
out.print(values.toString());
} catch (JSONException | IOException e) {
e.printStackTrace();
}
}
public static GoogleCredential createCredentialWithRefreshToken(HttpTransport transport,
JsonFactory jsonFactory,
GoogleClientSecrets clientSecrets,
TokenResponse tokenResponse) {
return new GoogleCredential.Builder().setTransport(transport)
.setJsonFactory(jsonFactory)
.setClientSecrets(clientSecrets)
.build()
.setFromTokenResponse(tokenResponse);
}
public static String transformName(HttpServletRequest request, Integer type){
switch(type) {
case 0:
return request.getScheme() + "://" + // "http" + "://
request.getServerName() + // "myhost"
":" + // ":"
request.getServerPort() + // "8080"
request.getRequestURI() + // "/people"
"?" + // "?"
request.getQueryString(); // "lastname=Fox&age=30"
case 1:
return request.getScheme() + "://" + // "http" + "://
request.getServerName() + // "myhost"
":" + // ":"
request.getServerPort() + // "8080"
request.getRequestURI(); // "/people"
case 2:
return request.getScheme() + "://" + // "http" + "://
request.getServerName() + // "myhost"
":" + // ":"
request.getServerPort(); // "8080"
default:
return request.getScheme() + "://" + // "http" + "://
request.getServerName() + // "myhost"
":" + // ":"
request.getServerPort() + // "8080"
request.getRequestURI() + // "/people"
"?" + // "?"
request.getQueryString(); // "lastname=Fox&age=30"
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
response.sendRedirect(transformName(request,2)+"/user");
}
private CustomOAuth2AuthenticationToken getOAuth2Token(User auth) {
return new CustomOAuth2AuthenticationToken(auth);
}
private GoogleClientSecrets loadSecret(String name){
ClassPathResource resource = new ClassPathResource(name);
try {
// Exchange auth code for access token
return GoogleClientSecrets.load(JacksonFactory.getDefaultInstance(), new FileReader(resource.getFile()));
} catch (IOException e) {
return null;
}
}
}
Also I changed the main Security class to:
private GoogleOAuth2Filter googleOAuth2Filter = new GoogleOAuth2Filter("/login/google");
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/", "/login/google", "/error**").permitAll().anyRequest().authenticated()
.and().exceptionHandling().authenticationEntryPoint((request, response, e) -> {
//create Json Object
try {
JSONObject values = new JSONObject();
values.put("principal", JSONObject.NULL);
values.put("authentication", JSONObject.NULL);
values.put("timestamp", String.valueOf(Timestamp.from(Instant.now())));
values.put("code",401);
values.put("message", "Not Authorized");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(values.toString());
} catch (JSONException | IOException f) {
f.printStackTrace();
}
})
.and().addFilterBefore(googleOAuth2Filter, BasicAuthenticationFilter.class);
// @formatter:on
}
Also I created custom mappings for /user and /logout.
Hope it can help someone in the future
Things get a lot easier if you use the EnableOAuth2Sso
method (though it hides a lot of the process from you). The Spring Boot tutorial on OAuth2 is quite thorough for this, and there are other examples online that I cribbed from (eg https://github.com/SoatGroup/spring-boot-google-auth/ and http://dreamix.eu/blog/java/configuring-google-as-oauth2-authorization-provider-in-spring-boot) that helped a little. Ultimately, this was the resource that helped me the most - covering the whole process and integration client side apps.
If you want to do this at a lower level, there is a lot of detail about the whole process and how it works in Spring on a Pivotal blog post.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With