Search code examples
javagoogle-directory-apigoogle-oauth-java-client

How do I change a user's password using Google's Directory API using Java?


I'm going to apologize in advance that I'm likely missing a few pieces of information to let anyone know that my settings are correct. Please let me know what else I need to specify. I work at a university where we host student email accounts through Google. It happens pretty frequently that they forget passwords and have to have it reset. We have a page that can set their password if they validate enough information about themselves. The code we've been using has been deprecated by Google in favor of their Directory API. I've been tasked with converting this old code:

//changes a password
@Override
public void reset ( final String username, final String password ) throws ResetException
{
    try
    {
        Validate.notEmpty( username );
        Validate.notEmpty( password );

        final AppsForYourDomainClient client = ClientFactory.getClient();  //admin-user account
        final UserEntry entry = client.retrieveUser( username );
        Validate.isTrue( !entry.getLogin().getSuspended(), "Account is suspended." );
        entry.getLogin().setPassword( password );
        client.updateUser( username, entry );
    }
    catch ( final Exception e )
    {
        throw new ResetException( e );
    }
}

into something using their new API. I've read a lot of the documentation and several examples, but none of them have seemed to help. I've enabled Admin SDK to our admin account through their Admin Console and registered my app and gotten a key from their Developer Console, but I can't seem to get any request to return what I want. Right now I'm just trying to get a list of users:

public void testList () throws Exception
{
    InputStream is = null;
    final String accessToken = getAccessToken();
    final NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
    final JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
    final File p12 = new File( GoogleResetterTest.class.getClassLoader().getResource("ead05e56893a.p12").toURI() );
    GoogleCredential credential = new GoogleCredential.Builder().setTransport(httpTransport)
                                        .setJsonFactory(jsonFactory)
                                        .setServiceAccountId( SERVICE_ACCOUNT_EMAIL )
                                        .setServiceAccountScopes( PlusScopes.all() )
                                        .setServiceAccountPrivateKeyFromP12File( p12 )  //password: notasecret
                                        .setClientSecrets(CLIENT_ID, CLIENT_SECRET)
                                        .build();
    final Directory dir = new Directory.Builder( httpTransport, jsonFactory, credential)
                                                .setApplicationName( "API Project" )
                                                .build();
    final Directory.Users diruser = dir.users();
    final Directory.Users.List diruserlist = diruser.list().setDomain( EMAIL_DOMAIN );
    final HttpResponse response = diruserlist.executeUsingHead();
    is = response.getContent();
    StringWriter writer = new StringWriter();
    IOUtils.copy(is, writer, "UTF-8");
    String theString = writer.toString();
    IOUtils.closeQuietly( is );
}

On the diruserlist.executeUsingHead(); line I get this response:

com.google.api.client.auth.oauth2.TokenResponseException: 400 Bad Request { "error" : "invalid_grant" }

To me this is a pretty useless error message because there seem to be 4 or 5 pieces that could go wrong to cause that response.

I can help thinking I'm making this whole thing too complicated. I liked the simplicity of the original code and some responses to the new API criticize that it's more complicated. Has anyone had to do this and could point me in the correct path to fix this?


Solution

  • I'm going to answer my own question and mark it as solved rather than erase the question or edit it.

    There were a few things at play here and some things that I got wrong. Mostly because the documentation on the builders is poor or non-existent. Here's what I had to do:

    Set up on the Admin Console (https://admin.google.com/)

    1. Make sure the account I'm trying to use has Super Admin access for this domain (Admin Roles -> Super Admin)
    2. Enable API Access in the Security tab. (Security -> API reference -> API access -> Enable API Access
    3. Do setup on the Developer Console(below)
    4. Add accesses to my app (Security -> Advanced settings -> Authentication -> Manage OAuth Client access
    5. Grant these scopes to my client from the Developer Console (without the square brackets):
      • [https]://www.googleapis.com/auth/admin.directory.group
      • [https]://www.googleapis.com/auth/admin.directory.user
      • [https]://www.googleapis.com/auth/admin.directory.user.readonly
      • [https]://www.googleapis.com/auth/admin.directory.user.security

    Not all of these scopes may be necessary, but it's what I have enabled

    Set up on the Developer Console (https://console.developers.google.com/)

    1. Create Project
    2. Enable Admin SDK for project (APIs & auth -> APIs)
    3. Create an OAuth client as a service account (APIs & auth -> Credentials -> Create new Client ID)
    4. Take that Client ID back to the Admin Console (step 5 above)
    5. Download p12 key where my java app can read it. (APIs & auth -> Credentials -> Generate new P12 key)

    Now I can finally write some java!

    import java.io.*;
    import java.net.*;
    import java.util.*;
    
    import java.security.GeneralSecurityException;
    import java.security.MessageDigest;
    
    import javax.xml.bind.DatatypeConverter;
    
    import com.google.api.services.admin.directory.*;
    import com.google.api.services.admin.directory.model.*;
    import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
    import com.google.api.client.http.javanet.NetHttpTransport;
    import com.google.api.client.json.jackson2.JacksonFactory;
    
    import org.apache.commons.lang.StringUtils;
    import org.apache.commons.lang.Validate;
    
    public class PasswordResetter
    {
        public void changePassword( final String username, final String newPassword ) throws Exception
        {
            final Directory dir = getDirectory();
            final User studentUser = updatePassword( getUser( dir, DOMAIN_NAME, username ), password );
            updateDirectory( dir, studentUser );
        }
    
        private Directory getDirectory() throws IOException, GeneralSecurityException, URISyntaxException
        {
            final NetHttpTransport httpTransport = new NetHttpTransport();
            final JacksonFactory jsonFactory = new JacksonFactory();
            final File p12 = new File( P12_FILENAME );
            final GoogleCredential credential = new GoogleCredential.Builder()
                                                                        .setTransport(httpTransport)
                                                                        .setJsonFactory(jsonFactory)
                                                                        .setServiceAccountUser( SUPER_USER_EMAIL )
                                                                        .setServiceAccountId( SERVICE_ACCOUNT_EMAIL_FROM_DEV_CONSOLE ) //the one that ends in "@developer.gserviceaccount.com"
                                                                        .setServiceAccountScopes( getCredentials() )
                                                                        .setServiceAccountPrivateKeyFromP12File( p12 )
                                                                        .build();
            return new Directory.Builder( httpTransport, jsonFactory, null)
                                        .setHttpRequestInitializer( credential )
                                        .setApplicationName( "API Project" )    //Not necessary, but silences a runtime warning using any not-blank string here
                                        .build();
        }
    
        private List<String> getCredentials()
        {
            final List<String> toReturn = new LinkedList<String>();
            toReturn.add( DirectoryScopes.ADMIN_DIRECTORY_GROUP );
            toReturn.add( DirectoryScopes.ADMIN_DIRECTORY_USER );
            toReturn.add( DirectoryScopes.ADMIN_DIRECTORY_USER_READONLY );
            toReturn.add( DirectoryScopes.ADMIN_DIRECTORY_USER_SECURITY );
            return toReturn;
        }
    
        private Users getUser( final Directory dir, final String domain, final String username ) throws Exception
        {
            Directory.Users.List diruserlist = dir.users().list()
                                                            .setDomain( domain )
                                                            .setQuery( "email:" + username + "*" );
            return diruserlist.execute();
        }
    
        private User updatePassword( final User user, final String password ) throws Exception
        {
            final MessageDigest md = MessageDigest.getInstance( "MD5" );    //I've been warned that this is not thread-safe
            final byte[] digested = md.digest( password.getBytes( "UTF-8" ) );
            final String newHashword = DatatypeConverter.printHexBinary( digested );
            return user.setHashFunction("MD5")                              //only accepts MD5, SHA-1, or CRYPT
                        .setPassword( newHashword );
        }
    
        private void updateDirectory( final Directory dir, final User user ) throws IOException
        {
            final Directory.Users.Update updateRequest = dir.users().update( user.getPrimaryEmail(), user );
            updateRequest.execute();
        }
    }
    

    Now I have a nice method for my super-user account to just supply a username (which defines their emails hence the query) and a new password and it changes it for them. Note this is not the full class and won't compile from just copy-paste.

    I surely hope I didn't skip a step. I have a few additional settings in my consoles that I'm afraid to get rid of in case it breaks something. Hopefully this will help someone else in the future.