Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot + Spring Security + Spring OAuth2 + Google Sign in

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

}
like image 560
Cristian Colorado Avatar asked Mar 10 '17 20:03

Cristian Colorado


People also ask

Is Google in OAuth2 sign in?

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.

How does OAuth2 2.0 work in spring boot?

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.


2 Answers

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

like image 158
HFR1994 Avatar answered Sep 29 '22 00:09

HFR1994


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.

like image 33
James Fry Avatar answered Sep 29 '22 00:09

James Fry