package org.molgenis.security.google; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload; import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import com.google.api.client.googleapis.auth.oauth2.GooglePublicKeysManager; import org.molgenis.auth.*; import org.molgenis.data.DataService; import org.molgenis.data.settings.AppSettings; import org.molgenis.security.core.token.UnknownTokenException; import org.molgenis.security.login.MolgenisLoginController; import org.molgenis.security.user.UserDetailsService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.UUID; import static java.lang.String.format; import static java.util.Objects.requireNonNull; import static org.molgenis.auth.GroupMemberMetaData.GROUP_MEMBER; import static org.molgenis.auth.GroupMetaData.GROUP; import static org.molgenis.auth.GroupMetaData.NAME; import static org.molgenis.auth.UserMetaData.*; import static org.molgenis.security.account.AccountService.ALL_USER_GROUP; import static org.molgenis.security.core.runas.RunAsSystemProxy.runAsSystem; import static org.springframework.http.HttpMethod.POST; public class GoogleAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { private static final Logger LOG = LoggerFactory.getLogger(GoogleAuthenticationProcessingFilter.class); public static final String GOOGLE_AUTHENTICATION_URL = "/login/google"; static final String PARAM_ID_TOKEN = "id_token"; private static final String PROFILE_KEY_GIVEN_NAME = "given_name"; private static final String PROFILE_KEY_FAMILY_NAME = "family_name"; private final GooglePublicKeysManager googlePublicKeysManager; private final DataService dataService; private final UserDetailsService userDetailsService; private final AppSettings appSettings; private final UserFactory userFactory; private final GroupMemberFactory groupMemberFactory; @Autowired public GoogleAuthenticationProcessingFilter(GooglePublicKeysManager googlePublicKeysManager, DataService dataService, UserDetailsService userDetailsService, AppSettings appSettings, UserFactory userFactory, GroupMemberFactory groupMemberFactory) { super(new AntPathRequestMatcher(GOOGLE_AUTHENTICATION_URL, POST.toString())); this.userFactory = requireNonNull(userFactory); this.groupMemberFactory = requireNonNull(groupMemberFactory); setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error")); this.googlePublicKeysManager = requireNonNull(googlePublicKeysManager); this.dataService = requireNonNull(dataService); this.userDetailsService = requireNonNull(userDetailsService); this.appSettings = requireNonNull(appSettings); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (appSettings.getGoogleSignIn() == false) { throw new AuthenticationServiceException("Google authentication not available"); } String idTokenString = request.getParameter(PARAM_ID_TOKEN); if (idTokenString != null) { // verify token string is valid GoogleIdToken idToken; try { idToken = verify(idTokenString); } catch (GeneralSecurityException e) { throw new UnknownTokenException(e.getMessage(), e); } // google token is null implies that verification failed if (idToken != null) { return createAuthentication(idToken.getPayload()); } else { throw new BadCredentialsException(format("Token [%s] verification failed", idTokenString)); } } throw new UnknownTokenException(idTokenString); } private GoogleIdToken verify(String idTokenString) throws GeneralSecurityException, IOException { List<String> audience = Collections.singletonList(appSettings.getGoogleAppClientId()); GoogleIdTokenVerifier googleIdTokenVerifier = new GoogleIdTokenVerifier.Builder(googlePublicKeysManager) .setAudience(audience).build(); return googleIdTokenVerifier.verify(idTokenString); } private Authentication createAuthentication(Payload payload) { String email = payload.getEmail(); if (email == null) { throw new AuthenticationServiceException( "Google ID token is missing required [email] claim, did you forget to specify scope [email]?"); } Boolean emailVerified = payload.getEmailVerified(); if (emailVerified != null && emailVerified.booleanValue() == false) { throw new AuthenticationServiceException("Google account email is not verified"); } String principal = payload.getSubject(); String credentials = payload.getAccessTokenHash(); return runAsSystem(() -> { User user; user = dataService.query(USER, User.class).eq(GOOGLEACCOUNTID, principal).findOne(); if (user == null) { // no user with google account user = dataService.query(USER, User.class).eq(EMAIL, email).findOne(); if (user != null) { // connect google account to user user.setGoogleAccountId(principal); dataService.update(USER, user); } else { // create new user String username = email; String givenName = payload.containsKey(PROFILE_KEY_GIVEN_NAME) ? payload.get(PROFILE_KEY_GIVEN_NAME) .toString() : null; String familyName = payload.containsKey(PROFILE_KEY_FAMILY_NAME) ? payload .get(PROFILE_KEY_FAMILY_NAME).toString() : null; user = createMolgenisUser(username, email, givenName, familyName, principal); } } if (!user.isActive()) { throw new DisabledException(MolgenisLoginController.ERROR_MESSAGE_DISABLED); } // create authentication Collection<? extends GrantedAuthority> authorities = userDetailsService.getAuthorities(user); return new UsernamePasswordAuthenticationToken(user.getUsername(), credentials, authorities); }); } private User createMolgenisUser(String username, String email, String givenName, String familyName, String googleAccountId) { if (!appSettings.getSignUp()) { throw new AuthenticationServiceException("Google authentication not possible: sign up disabled"); } if (appSettings.getSignUpModeration()) { throw new AuthenticationServiceException("Google authentication not possible: sign up moderation enabled"); } // create user LOG.info("first login for [{}], creating MOLGENIS user", username); User user = userFactory.create(); user.setUsername(username); user.setPassword(UUID.randomUUID().toString()); user.setEmail(email); user.setActive(true); user.setSuperuser(false); user.setChangePassword(false); if (givenName != null) { user.setFirstName(givenName); } if (familyName != null) { user.setLastName(familyName); } user.setGoogleAccountId(googleAccountId); dataService.add(USER, user); // add user to all-users group GroupMember groupMember = groupMemberFactory.create(); Group group = dataService.query(GROUP, Group.class).eq(NAME, ALL_USER_GROUP).findOne(); groupMember.setGroup(group); groupMember.setUser(user); dataService.add(GROUP_MEMBER, groupMember); return user; } }