18

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;
    }

}

2 Answers 2

9

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

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for sharing!
5

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.