個人的に認証周りの実装は好きだが、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
シーケンスとしては以下のようになるが、以下の流れ(ブラウザ起動 > 認証リクエスト > 認証 > アクセストークン発行)を全て自動化してみた。
環境:
Java: 1.8
Selenium: 3.12.0
ChromeDriver: 77.0.3865.40
Chrome: 77.0.3865.90
今回ビルドツールには、Mavenを使用したので、以下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>
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"}