4
5

More than 3 years have passed since last update.

Google OAuthを二段階認証含め全て自動化してみた

Last updated at Posted at 2019-10-17

個人的に認証周りの実装は好きだが、Google OAuthの実装はやってなかったので、この機会に実装してみた。

OAuthの機能は、FaceBookやYahoo、Twitterなど色んなベンダーが提供しているが、Googleに関しては都度JavaScriptで動的にパラメータを生成しているので、認証部分はchromedriver/Selenium(スクレイピングライブラリ)を使いブラウザベースの自動認証を実装することにした。

また、厄介なのが自動ログインの場合、Googleはユーザ名・パスワードに加え、二段階認証を要求してくるので、自動化しがいがかなりある。。
自動ログインを試みた際は、Googleからメールで二段階認証用の認証コードが再設定用メールアドレスに送られてくるが、今回はこのメールをAWS SESを用いた受信専用メールサーバへ飛ばし、そのメールをS3へ格納した後に、メール本文に記載してある認証コードを自動で読み取り、その値を自動入力するようにした。

SESの設定は、今更感があるが念の為以下に参考URLを載せておく。
https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/receiving-email-setting-up.html

シーケンスとしては以下のようになるが、以下の流れ(ブラウザ起動 > 認証リクエスト > 認証 > アクセストークン発行)を全て自動化してみた。

スクリーンショット 2019-10-18 3.43.28.png

環境:
 Java: 1.8
 Selenium: 3.12.0
 ChromeDriver: 77.0.3865.40
 Chrome: 77.0.3865.90

今回ビルドツールには、Mavenを使用したので、以下pom.xmlの内容。

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>googleOAuth</groupId>
  <artifactId>googleOAuth</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>googleOAuth</name>
  <dependencies>
  <dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>3.12.0</version>
</dependency>
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-s3</artifactId>
    <version>1.11.651</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.7</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.7</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.9.7</version>
</dependency>
<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-java-sdk-sts</artifactId>
    <version>1.9.6</version>
</dependency>
</dependencies>
  <build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.2.1</version>
        </plugin>
              <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
            <archive>
              <manifest>
                <mainClass>googleOAuth.GoogleOAuth2</mainClass>
              </manifest>
            </archive>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

GoogleOAuth.java
package google;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import com.amazonaws.util.IOUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class GoogleOAuth {

    private final String clientId;
    private final String clientSecret;
    private final String redirectUri;
    private final String scope;
    private final String responseType;
    private final String tokenUrl;
    private final String driverpath;
    private String emailAddress;
    private String password;
    private String recEmailAddress;
    private String lastName;
    private String firstName;
    private String BucketName;
    private String LocalPath;

    public GoogleOAuth() {
        clientId = "*****"; //クライアントID
        clientSecret = "*****"; //クライアントシークレット
        redirectUri = "*****"; //リダイレクトURI
        scope = "https://www.googleapis.com/auth/userinfo.profile";
        responseType = "code";
        tokenUrl = "https://accounts.google.com/o/oauth2/token";
        BucketName = "*****"; //S3バケット名
        LocalPath = "*****"; //メール保存パス
        driverpath = "*****"; //chromedriverパス
        emailAddress = "*****"; //ログインメールアドレス
        password = "*****"; //パスワード
        recEmailAddress = "*****"; //SES受信メールサーバアドレス
        lastName = "*****"; //LastName
        firstName = "*****"; //FirstName
    }

    public static void main(String[] args) throws IOException {
        GoogleOAuth google = new GoogleOAuth();
        String aZcode = google.getAzCode();
        JsonNode access_token = google.getAccessToken(aZcode);
        System.out.println(access_token);

    }

    public String getAzCode() throws IOException {
        final String PATH = this.driverpath;
        System.setProperty("webdriver.chrome.driver", PATH);
        WebDriver driver = new ChromeDriver();
        final String URL = "https://accounts.google.com/o/oauth2/auth?client_id=" + this.clientId + "&redirect_uri=" +
                this.redirectUri + "&scope=" + this.scope + "&response_type=" + this.responseType;

        driver.get(URL);


        try {
            driver.findElement(By.xpath("//*[@id=\"identifierId\"]")).sendKeys(this.emailAddress);;
            driver.findElement(By.xpath("//*[@id=\"yDmH0d\"]")).click();
            Thread.sleep(5000);

            driver.findElement(By.xpath("//*[@id=\"recoveryIdentifierId\"]")).sendKeys(this.recEmailAddress);;
            driver.findElement(By.xpath("//*[@id=\"queryPhoneNext\"]/span/span")).click();
            Thread.sleep(5000);

            driver.findElement(By.xpath("//*[@id=\"lastName\"]")).sendKeys(this.lastName);;
            driver.findElement(By.xpath("//*[@id=\"firstName\"]")).sendKeys(this.firstName);;
            driver.findElement(By.xpath("//*[@id=\"collectNameNext\"]/span/span")).click();
            Thread.sleep(5000);

            driver.findElement(By.xpath("//*[@id=\"idvpreregisteredemailNext\"]/span/span")).click();
            Thread.sleep(5000);

            this.downloadFile();
            Thread.sleep(3000);
            String token = this.getToken();

            driver.findElement(By.xpath("//*[@id=\"idvPinId\"]")).sendKeys(token);;
            driver.findElement(By.xpath("//*[@id=\"idvpreregisteredemailNext\"]/span")).click();
            Thread.sleep(3000);

            driver.findElement(By.xpath("//*[@id=\"view_container\"]/div/div/div[2]/div/div/div/form/span/section/div/div/div/div/ul/li[3]/div/div/div[2]")).click();
            Thread.sleep(3000);

            driver.findElement(By.xpath("//*[@id=\"identifierId\"]")).sendKeys(this.emailAddress);;
            driver.findElement(By.xpath("//*[@id=\"identifierNext\"]/span/span")).click();
            Thread.sleep(3000);

            driver.findElement(By.xpath("//*[@id=\"password\"]/div[1]/div/div[1]/input")).sendKeys(this.password);;
            driver.findElement(By.xpath("//*[@id=\"passwordNext\"]/span/span")).click();
            Thread.sleep(3000);
            } catch (InterruptedException e) {
                System.out.println("Caught InterruptedException: " + e);
            }

            String currentUrl = driver.getCurrentUrl();
            //System.out.println(currentUrl);
            int biginIdx = currentUrl.indexOf("code=");
            int endIdx = currentUrl.indexOf("&scope");
            String aZcode = currentUrl.substring(biginIdx+5, endIdx);

            driver.quit();

            return aZcode;
        }


    public JsonNode getAccessToken(String code) throws IOException {
        URL url = new URL(this.tokenUrl);
        StringBuffer result = new StringBuffer();
        HttpURLConnection  conn = null;
        conn = (HttpURLConnection) url.openConnection();
        conn.setDoOutput(true);
        conn.setRequestMethod("POST");
        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
        conn.setUseCaches(false);
        String params = new String("code=" + code + "&client_id=" + this.clientId + "&client_secret=" + this.clientSecret +
                "&redirect_uri=" + this.redirectUri + "&grant_type=authorization_code");
        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
        out.write(params);
        out.close();
        conn.connect();

        final int status = conn.getResponseCode();
        if (status == 200) {
            final InputStream input = conn.getInputStream();
            String encoding = conn.getContentEncoding();
            if(null == encoding){
                encoding = "UTF-8";
            }
            final InputStreamReader inReader = new InputStreamReader(input, encoding);
            final BufferedReader bufReader = new BufferedReader(inReader);
            String line = null;
            while((line = bufReader.readLine()) != null) {
                result.append(line);
            }
            bufReader.close();
            inReader.close();
            input.close();
        } else {
            System.out.println(status);
        }

        conn.disconnect();

        ObjectMapper mapper = new ObjectMapper();
        JsonNode root = mapper.readTree(result.toString());

        return root;
    }

    public void downloadFile() throws IOException {
        AmazonS3 s3 = AmazonS3ClientBuilder.standard()
                .withRegion("us-east-1")
                .build();

        ObjectListing objectListing = s3.listObjects(new ListObjectsRequest()
                .withBucketName(this.BucketName));

        for (S3ObjectSummary objectSummary : objectListing.getObjectSummaries()) {
            System.out.println(objectSummary.getKey());
            String objectKey = objectSummary.getKey();
            S3Object object = s3.getObject(new GetObjectRequest(this.BucketName, objectKey));
            try {
                FileOutputStream fos = new FileOutputStream(new File(this.LocalPath));
                IOUtils.copy(object.getObjectContent(), fos);
                fos.close();
                s3.deleteObject(new DeleteObjectRequest(this.BucketName, objectKey));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public String getToken() throws IOException {
        File file = new File(this.LocalPath);
        FileReader fr = new FileReader(file);
        BufferedReader br = new BufferedReader(fr);
        String json_str;
        String data = "";
        int cnt = 0;

        while ((json_str = br.readLine()) != null) {
            cnt += 1;
            if ((cnt>= 73)&&(128 >= cnt)){
            data += json_str;
            }
        }

        Charset charset = StandardCharsets.UTF_8;
        byte[] bytes = Base64.getDecoder().decode(data.getBytes());
        String decode_str = new String(bytes, charset);
        int biginIdx = decode_str.indexOf("<strong style=\"text-align: center; font-size: 24px; font-weight: bold;\">");
        int endIdx = decode_str.indexOf("</strong>");
        String token = decode_str.substring(biginIdx+72, endIdx);

        br.close();
        return token;
    }
}

上記コードを実行し、認証/認可が完了すると、最終的に以下の通りアクセストークンが取得できる。

{"access_token":"ya29.ImCiBwhJcCZ-JIW9ZlGDe2ysCBkNzRpG9mgr_-ocM_A32Dh1bzJbHAHOT_iKi6GNAVovWDLgyKclHJaP1uqTODQ61LJomWAUzhWSnMsd4ddGKTeIUfOeSQocVbUzikxcWjU","expires_in":3596,"scope":"https://www.googleapis.com/auth/userinfo.profile","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IjNkYjNlZDZiOTU3NGVlM2ZjZDlmMTQ5ZTU5ZmYwZWVmNGY5MzIxNTMiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwiYXpwIjoiNjUwMDQxMDk4ODAzLTBtMG81Y2IxZ2ZraW43cDJwN2ZuaWNhYnUxaWdvdnVnLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNjUwMDQxMDk4ODAzLTBtMG81Y2IxZ2ZraW43cDJwN2ZuaWNhYnUxaWdvdnVnLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTE1OTcwNTg3NjcwMjI4MTk2Njc4IiwiYXRfaGFzaCI6IjhGZmJSa3BYQ1VoWUduX0VTN2tENnciLCJpYXQiOjE1NzEzMzE4ODYsImV4cCI6MTU3MTMzNTQ4Nn0.oXK4EhivivgJqhO6cjJ7ZyeTBPW9IrTOvM_GGeDcNJ6XXTOVtRd8nv3pRT1SHDSTTgkco6tAI8ZN-dWDD-LVhqkhGTjM-USzR2UbXhMsd4z6WRYGMPfzuLCkGRGZwp0Gqd7ctITlIWJr0N8xLPEGNSHx_IPZtabOaP4ME9YN0-XQg-x2tE32LdRI4ttvW7D1lSeYPKdzWf_12i9zoLnblc2MiGTupww91PlQSaLGoprFPVutI_57c1IraBsCmMGuW0Nke4vps7YoFhaaxDhB4XuJCpyIvpADHuOydq3RPY5WCV8YIzFXsFCa1f-Z7QyJNuZgcFf7WLWcjXQZkD4i8g"}
4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5