web-dev-qa-db-fra.com

Spring Boot + Spring Security + Spring OAuth2 + Google

J'ai configuré un petit projet pour implémenter la connexion OAuth2 avec l'API Google+, en utilisant Spring Boot (1.5.2), Spring Security et Spring Security OAuth2.

Vous pouvez trouver la source dans: https://github.com/ccoloradoc/OAuth2Sample

Je peux m'authentifier auprès de Google et extraire les informations utilisateur. Cependant, après la déconnexion, je ne peux plus me connecter car j'ai reçu une "400 mauvaise demande", après avoir tenté de me connecter " https://accounts.google.com/o/oauth2/auth " avec mon RestTemplate pour appeler Google api.

Voir la méthode Filter tryAuthentication pour plus de référence.

Voici ma classe de configuration de sécurité

@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();
    }

}

Voici mon fournisseur d'authentification:

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 est une implémentation du noyau de sécurité Spring qui lit l'utilisateur de la base de données et le traduit en POJO UserDetails qui implémente le noyau de sécurité Spring UserDetails.

Voici mon implémentation de filtre:

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

}
15
Cristian Colorado

Merci Cristian, tu n'as aucune idée de combien ton code a aidé à démarrer une fondation pour mon propre code. J'ai modifié votre projet Github OAuth2 d'origine et l'ai changé en le code suivant.

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

}

J'ai également changé la classe de sécurité principale en:

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
}

J'ai également créé des mappages personnalisés pour/user et/logout.

J'espère que cela peut aider quelqu'un à l'avenir

6
HFR1994

Les choses deviennent beaucoup plus faciles si vous utilisez le EnableOAuth2Sso méthode (bien qu'elle vous cache une grande partie du processus). Le tutoriel Spring Boot sur OAuth2 est assez complet pour cela, et il y a d'autres exemples en ligne à partir desquels j'ai cribué (par exemple https://github.com/SoatGroup/spring-boot-google -auth / et http://dreamix.eu/blog/Java/configuring-google-as-oauth2-authorization-provider-in-spring-boot ) qui a un peu aidé. En fin de compte, c'était la ressource qui m'a le plus aidé - couvrant l'ensemble du processus et l'intégration des applications côté client.

Si vous voulez le faire à un niveau inférieur, il y a beaucoup de détails sur l'ensemble du processus et comment il fonctionne au printemps sur un Pivotal blog post .

5
James Fry