Spring Boot + Spring Security + Spring OAuth2 + Google Sign in
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@Overridepublic void setAuthenticationManager(AuthenticationManager authenticationManager) { super.setAuthenticationManager(authenticationManager);}@Overridepublic 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" }}@Overrideprotected 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");@Overrideprotected 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.