ログインの繰り返しを処理する

これは、Classroom アドオン チュートリアル シリーズの 3 番目のチュートリアルです。

このチュートリアルでは、以前に付与されたユーザーの認証情報を自動的に取得することで、アドオンへの再アクセスを処理します。その後、ユーザーがすぐに API リクエストを発行できるページにユーザーを誘導します。これは、Classroom アドオンに必須の動作です。

このチュートリアルでは、次のことを行います。

  • ユーザー認証情報用の永続ストレージを実装します。
  • 次のアドオン クエリ パラメータを取得して評価します。
    • login_hint: ログイン ユーザーの Google ID 番号。
    • hd: ログイン ユーザーのドメイン。

このうち 1 つのみが送信されます。ユーザーがアプリをまだ承認していない場合は、Classroom API から hd パラメータが送信されます。それ以外の場合は login_hint を送信します。クエリ パラメータの一覧については、iframe ガイドページをご覧ください。

完了すると、ウェブアプリでユーザーを完全に承認し、Google API の呼び出しを発行できます。

iframe クエリ パラメータについて

アドオンの添付ファイルの設定 URI が Classroom に読み込まれます。Classroom では、いくつかの GET クエリ パラメータが URI に追加されます。これらのパラメータには、有用なコンテキスト情報が含まれています。たとえば、添付ファイルの検出 URI が https://example.com/addon の場合、Classroom はソース URL を https://example.com/addon?courseId=XXX&postId=YYY&addOnToken=ZZZ に設定した iframe を作成します。ここで、XXXYYYZZZ は文字列 ID です。このシナリオの詳細については、iframe ガイドをご覧ください。

検出 URL のクエリ パラメータは次の 5 つです。

  • courseId: 現在の Classroom コースの ID。
  • postId: ユーザーが編集または作成している課題投稿の ID。
  • addOnToken: Classroom の特定のアドオン アクションを承認するために使用されるトークン。
  • login_hint: 現在のユーザーの Google ID。
  • hd: 現在のユーザーのホストドメイン(example.com など)。

このチュートリアルでは、hdlogin_hint を取り上げます。ユーザーは、指定されたクエリ パラメータに基づいて、hd の場合は承認フローに、login_hint の場合はアドオン検出ページにルーティングされます。

クエリ パラメータにアクセスする

前述のように、クエリ パラメータは URI 文字列でウェブ アプリケーションに渡されます。これらの値をセッションに保存します。これらの値は、承認フローで使用され、ユーザーに関する情報の保存と取得に使用されます。これらのクエリ パラメータは、アドオンが最初に開いたときにのみ渡されます。

Python

Flask のルートの定義に移動します(上記の例を使用している場合は routes.py)。アドオンのランディング ルートの先頭(この例では /classroom-addon)で、login_hinthd のクエリ パラメータを取得して保存します。

# Retrieve the login_hint and hd query parameters.
login_hint = flask.request.args.get("login_hint")
hd = flask.request.args.get("hd")

login_hinthd がセッションに保存されていることを確認します。これはこれらの値を格納するのに適した場所です。これらの値は一時的なものであり、アドオンを開くと新しい値を受け取ります。

# It's possible that we might return to this route later, in which case the
# parameters will not be passed in. Instead, use the values cached in the
# session.

# If neither query parameter is available, use the values in the session.
if login_hint is None and hd is None:
    login_hint = flask.session.get("login_hint")
    hd = flask.session.get("hd")

# If there's no login_hint query parameter, then check for hd.
# Send the user to the sign in page.
elif hd is not None:
    flask.session["hd"] = hd
    return start_auth_flow()

# If the login_hint query parameter is available, we'll store it in the
# session.
else:
    flask.session["login_hint"] = login_hint

Java

コントローラ クラスのアドオン ランディング ルートに移動します(この例では AuthController.java/addon-discovery)。このルートの最初に、login_hint クエリ パラメータと hd クエリ パラメータを取得して保存します。

/** Retrieve the login_hint or hd query parameters from the request URL. */
String login_hint = request.getParameter("login_hint");
String hd = request.getParameter("hd");

login_hinthd がセッションに保存されていることを確認します。これはこれらの値を格納するのに適した場所です。これらの値は一時的なものであり、アドオンを開くと新しい値を受け取ります。

/** If neither query parameter is sent, use the values in the session. */
if (login_hint == null && hd == null) {
    login_hint = (String) session.getAttribute("login_hint");
    hd = (String) session.getAttribute("hd");
}

/** If the hd query parameter is provided, add hd to the session and route
*   the user to the authorization page. */
else if (hd != null) {
    session.setAttribute("hd", hd);
    return startAuthFlow(model);
}

/** If the login_hint query parameter is provided, add it to the session. */
else if (login_hint != null) {
    session.setAttribute("login_hint", login_hint);
}

クエリ パラメータを承認フローに追加する

login_hint パラメータと hd パラメータも Google の認証サーバーに渡す必要があります。これにより、認証プロセスが容易になります。認証対象のユーザーがアプリケーションによってわかっている場合、サーバーはこのヒントを使用してログイン フォームのメールアドレス フィールドに事前に入力し、ログインフローを簡素化します。

Python

Flask サーバー ファイル内の認証ルートに移動します(この例では /authorize)。flow.authorization_url の呼び出しに login_hint 引数と hd 引数を追加します。

authorization_url, state = flow.authorization_url(
    # Enable offline access so that you can refresh an access token without
    # re-prompting the user for permission. Recommended for web server apps.
    access_type="offline",
    # Enable incremental authorization. Recommended as a best practice.
    include_granted_scopes="true",
    # The user will automatically be selected if we have the login_hint.
    login_hint=flask.session.get("login_hint"),
    # If we don't have login_hint, passing hd will reduce the list of
    # accounts in the account chooser to only those with the same domain.
    hd=flask.session.get("hd"))

Java

AuthService.java クラスの authorize() メソッドに移動します。このメソッドにパラメータとして login_hinthd を追加し、login_hinthd の引数を承認 URL ビルダーに追加します。

String authUrl = flow
    .newAuthorizationUrl()
    .setState(state)
    .set("login_hint", login_hint)
    .set("hd", hd)
    .setRedirectUri(REDIRECT_URI)
    .build();

ユーザー認証情報用の永続ストレージを追加する

アドオンの読み込み時にクエリ パラメータとして login_hint を受け取った場合は、ユーザーがアプリケーションの承認フローをすでに完了しています。ログインを強制するのではなく、以前の認証情報を取得する必要があります。

承認フローの完了時に更新トークンを受け取ったことを思い出してください。このトークンを保存します。アクセス トークンを取得するために再利用します。アクセス トークンは有効期間が短く、Google API を使用するために必要です。以前にこれらの認証情報をセッションに保存しましたが、再アクセスを処理するには認証情報を保存する必要があります。

ユーザー スキーマを定義してデータベースを設定する

User のデータベース スキーマを設定します。

Python

ユーザー スキーマを定義する

User には次の属性が含まれます。

  • id: ユーザーの Google ID。これは、login_hint クエリ パラメータで指定された値と一致する必要があります。
  • display_name: ユーザーの氏名(「Alex Smith」など)。
  • email: ユーザーのメールアドレス。
  • portrait_url: ユーザーのプロフィール写真の URL。
  • refresh_token: 以前に取得した更新トークン。

この例では、Python でネイティブにサポートされている SQLite を使用してストレージを実装します。データベース管理を容易にするために flask_sqlalchemy モジュールを使用します。

データベースを設定する

まず、データベースのファイルの場所を指定します。サーバー構成ファイル(この例では config.py)に移動し、以下を追加します。

import os

# Point to a database file in the project root.
DATABASE_FILE_NAME = os.path.join(
    os.path.abspath(os.path.dirname(__file__)), 'data.sqlite')

class Config(object):
    SQLALCHEMY_DATABASE_URI = f"sqlite:///{DATABASE_FILE_NAME}"
    SQLALCHEMY_TRACK_MODIFICATIONS = False

これにより、Flask は main.py ファイルと同じディレクトリにある data.sqlite ファイルを指します。

次に、モジュール ディレクトリに移動して、新しい models.py ファイルを作成します。示された例に従っている場合、これは webapp/models.py です。新しいファイルに以下を追加して User テーブルを定義します。異なる場合は、webapp をモジュール名に置き換えます。

from webapp import db

# Database model to represent a user.
class User(db.Model):
    # The user's identifying information:
    id = db.Column(db.String(120), primary_key=True)
    display_name = db.Column(db.String(80))
    email = db.Column(db.String(120), unique=True)
    portrait_url = db.Column(db.Text())

    # The user's refresh token, which will be used to obtain an access token.
    # Note that refresh tokens will become invalid if:
    # - The refresh token has not been used for six months.
    # - The user revokes your app's access permissions.
    # - The user changes passwords.
    # - The user belongs to a Google Cloud organization
    #   that has session control policies in effect.
    refresh_token = db.Column(db.Text())

最後に、モジュールの __init__.py ファイルに以下を追加して、新しいモデルをインポートし、データベースを作成します。

from webapp import models
from os import path
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy(app)

# Initialize the database file if not created.
if not path.exists(config.DATABASE_FILE_NAME):
    db.create_all()

Java

ユーザー スキーマを定義する

User には次の属性が含まれます。

  • id: ユーザーの Google ID。これは、login_hint クエリ パラメータで指定された値と一致する必要があります。
  • email: ユーザーのメールアドレス。

モジュールの resources ディレクトリに schema.sql ファイルを作成します。Spring はこのファイルを読み取り、それに応じてデータベースのスキーマを生成します。テーブル名 users と、User 属性 idemail を表す列を指定してテーブルを定義します。

CREATE TABLE IF NOT EXISTS users (
    id VARCHAR(255) PRIMARY KEY, -- user's unique Google ID
    email VARCHAR(255), -- user's email address
);

Java クラスを作成して、データベースの User モデルを定義します。この例では User.java です。

@Entity アノテーションを追加して、これがデータベースに保存できる POJO であることを示します。schema.sql で構成した対応するテーブル名を持つ @Table アノテーションを追加します。

サンプルコードには、2 つの属性のコンストラクタとセッターが含まれています。コンストラクタとセッターは、データベース内でユーザーを作成または更新するために AuthController.java で使用されます。必要に応じてゲッターと toString メソッドを含めることもできますが、このチュートリアルではこれらのメソッドは使用せず、簡潔にするためにこのページのコード例では省略します。

/** An entity class that provides a model to store user information. */
@Entity
@Table(name = "users")
public class User {
    /** The user's unique Google ID. The @Id annotation specifies that this
     *   is the primary key. */
    @Id
    @Column
    private String id;

    /** The user's email address. */
    @Column
    private String email;

    /** Required User class no args constructor. */
    public User() {
    }

    /** The User class constructor that creates a User object with the
    *   specified parameters.
    *   @param id the user's unique Google ID
    *   @param email the user's email address
    */
    public User(String id, String email) {
        this.id = id;
        this.email = email;
    }

    public void setId(String id) { this.id = id; }

    public void setEmail(String email) { this.email = email; }
}

データベースに対する CRUD オペレーションを処理する UserRepository.java というインターフェースを作成します。このインターフェースは CrudRepository インターフェースを拡張します。

/** Provides CRUD operations for the User class by extending the
 *   CrudRepository interface. */
@Repository
public interface UserRepository extends CrudRepository<User, String> {
}

コントローラ クラスは、クライアントとリポジトリ間の通信を容易にします。したがって、コントローラ クラスのコンストラクタを更新して、UserRepository クラスを注入します。

/** Declare UserRepository to be used in the Controller class constructor. */
private final UserRepository userRepository;

/**
*   ...
*   @param userRepository the class that interacts with User objects stored in
*   persistent storage.
*/
public AuthController(AuthService authService, UserRepository userRepository) {
    this.authService = authService;
    this.userRepository = userRepository;
}

データベースを設定する

ユーザー関連の情報を保存するには、Spring Boot で本質的にサポートされている H2 データベースを使用します。このデータベースは、後続のチュートリアルで他の Classroom 関連情報を保存するためにも使用されます。H2 データベースを設定するには、application.properties に次の構成を追加する必要があります。

# Enable configuration for persistent storage using an H2 database
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:file:./h2/userdb
spring.datasource.username=<USERNAME>
spring.datasource.password=<PASSWORD>
spring.jpa.hibernate.ddl-auto=update
spring.jpa.open-in-view=false

spring.datasource.url 構成ファイルは h2 というディレクトリを作成し、その中に格納ファイル userdb を格納します。H2 データベースへのパスを .gitignore に追加します。アプリケーションを実行する前に、spring.datasource.usernamespring.datasource.password を更新して、任意のユーザー名とパスワードでデータベースを設定する必要があります。アプリケーションの実行後にデータベースのユーザー名とパスワードを更新するには、生成された h2 ディレクトリを削除し、構成を更新して、アプリケーションを再実行します。

spring.jpa.hibernate.ddl-auto 構成を update に設定すると、アプリを再起動したときに、データベースに保存されているデータが保持されます。アプリケーションを再起動するたびにデータベースをクリアするには、この構成を create に設定します。

spring.jpa.open-in-view 構成を false に設定します。この構成はデフォルトで有効になっており、本番環境で診断が困難なパフォーマンスの問題が発生することがわかっています。

前述のように、リピーターの認証情報を取得できる必要があります。これは、GoogleAuthorizationCodeFlow が提供する組み込みの認証情報ストア サポートによって実現されます。

AuthService.java クラスで、認証情報クラスが保存されているファイルのパスを定義します。この例では、ファイルは /credentialStore ディレクトリに作成されます。認証情報ストアのパスを .gitignore に追加します。このディレクトリは、ユーザーが承認フローを開始すると生成されます。

private static final File dataDirectory = new File("credentialStore");

次に、FileDataStoreFactory オブジェクトを作成して返すメソッドを AuthService.java ファイルに作成します。これは、認証情報を格納するデータストアです。

/** Creates and returns FileDataStoreFactory object to store credentials.
 *   @return FileDataStoreFactory dataStore used to save and obtain users ids
 *   mapped to Credentials.
 *   @throws IOException if creating the dataStore is unsuccessful.
 */
public FileDataStoreFactory getCredentialDataStore() throws IOException {
    FileDataStoreFactory dataStore = new FileDataStoreFactory(dataDirectory);
    return dataStore;
}

AuthService.javagetFlow() メソッドを更新して GoogleAuthorizationCodeFlow Builder() メソッドに setDataStoreFactory を追加し、getCredentialDataStore() を呼び出してデータストアを設定します。

GoogleAuthorizationCodeFlow authorizationCodeFlow =
    new GoogleAuthorizationCodeFlow.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY,
        getClientSecrets(),
        getScopes())
    .setAccessType("offline")
    .setDataStoreFactory(getCredentialDataStore())
    .build();

次に、getAndSaveCredentials(String authorizationCode) メソッドを更新します。これまで、このメソッドはどこにも保存せずに認証情報を取得していました。メソッドを更新して、ユーザー ID でインデックス付けされたデータストアに認証情報を保存します。

ユーザー ID は、id_token を使用して TokenResponse オブジェクトから取得できますが、まず検証する必要があります。そうしないと、変更したユーザー ID をサーバーに送信することで、クライアント アプリケーションがユーザーの権限を借用できる可能性があります。Google API クライアント ライブラリを使用して id_token を検証することをおすすめします。詳しくは、[Google ID トークンの検証に関する Google Identity ページ] をご覧ください。

// Obtaining the id_token will help determine which user signed in to the application.
String idTokenString = tokenResponse.get("id_token").toString();

// Validate the id_token using the GoogleIdTokenVerifier object.
GoogleIdTokenVerifier googleIdTokenVerifier = new GoogleIdTokenVerifier.Builder(
        HTTP_TRANSPORT,
        JSON_FACTORY)
    .setAudience(Collections.singletonList(
        googleClientSecrets.getWeb().getClientId()))
    .build();

GoogleIdToken idToken = googleIdTokenVerifier.verify(idTokenString);

if (idToken == null) {
    throw new Exception("Invalid ID token.");
}

id_token を確認したら、取得した認証情報とともに保存する userId を取得します。

// Obtain the user id from the id_token.
Payload payload = idToken.getPayload();
String userId = payload.getSubject();

flow.createAndStoreCredential の呼び出しを更新して、userId を含めます。

// Save the user id and credentials to the configured FileDataStoreFactory.
Credential credential = flow.createAndStoreCredential(tokenResponse, userId);

特定のユーザーの認証情報がデータストアに存在する場合に認証情報を返すメソッドを AuthService.java クラスに追加します。

/** Find credentials in the datastore based on a specific user id.
*   @param userId key to find in the file datastore.
*   @return Credential object to be returned if a matching key is found in the datastore. Null if
*   the key doesn't exist.
*   @throws Exception if building flow object or checking for userId key is unsuccessful. */
public Credential loadFromCredentialDataStore(String userId) throws Exception {
    try {
        GoogleAuthorizationCodeFlow flow = getFlow();
        Credential credential = flow.loadCredential(userId);
        return credential;
    } catch (Exception e) {
        e.printStackTrace();
        throw e;
    }
}

認証情報を取得する

Users を取得するメソッドを定義します。login_hint クエリ パラメータで id が提供されており、これを使用して特定のユーザー レコードを取得できます。

Python

def get_credentials_from_storage(id):
    """
    Retrieves credentials from the storage and returns them as a dictionary.
    """
    return User.query.get(id)

Java

AuthController.java クラスで、ユーザー ID に基づいてデータベースからユーザーを取得するメソッドを定義します。

/** Retrieves stored credentials based on the user id.
*   @param id the id of the current user
*   @return User the database entry corresponding to the current user or null
*   if the user doesn't exist in the database.
*/
public User getUser(String id) {
    if (id != null) {
        Optional<User> user = userRepository.findById(id);
        if (user.isPresent()) {
            return user.get();
        }
    }
    return null;
}

認証情報を保存する

認証情報を保存する場合、2 つのシナリオがあります。ユーザーの id がすでにデータベースにある場合、既存のレコードを新しい値で更新します。それ以外の場合は、新しい User レコードを作成してデータベースに追加します。

Python

まず、ストレージまたは更新の動作を実装するユーティリティ メソッドを定義します。

def save_user_credentials(credentials=None, user_info=None):
    """
    Updates or adds a User to the database. A new user is added only if both
    credentials and user_info are provided.

    Args:
        credentials: An optional Credentials object.
        user_info: An optional dict containing user info returned by the
            OAuth 2.0 API.
    """

    existing_user = get_credentials_from_storage(
        flask.session.get("login_hint"))

    if existing_user:
        if user_info:
            existing_user.id = user_info.get("id")
            existing_user.display_name = user_info.get("name")
            existing_user.email = user_info.get("email")
            existing_user.portrait_url = user_info.get("picture")

        if credentials and credentials.refresh_token is not None:
            existing_user.refresh_token = credentials.refresh_token

    elif credentials and user_info:
        new_user = User(id=user_info.get("id"),
                        display_name=user_info.get("name"),
                        email=user_info.get("email"),
                        portrait_url=user_info.get("picture"),
                        refresh_token=credentials.refresh_token)

        db.session.add(new_user)

    db.session.commit()

認証情報をデータベースに保存するインスタンスは 2 つあります。ユーザーが認可フローの最後でアプリケーションに戻ったときと、API 呼び出しを発行したときです。ここに、以前にセッション credentials キーを設定しました。

callback ルートの終点で save_user_credentials を呼び出します。ユーザー名を抽出するだけではなく、user_info オブジェクトを保持してください。

# The flow is complete! We'll use the credentials to fetch the user's info.
user_info_service = googleapiclient.discovery.build(
    serviceName="oauth2", version="v2", credentials=credentials)

user_info = user_info_service.userinfo().get().execute()

flask.session["username"] = user_info.get("name")

save_user_credentials(credentials, user_info)

また、API の呼び出し後に認証情報を更新する必要もあります。この場合、更新された認証情報を save_user_credentials メソッドの引数として指定できます。

# Save credentials in case access token was refreshed.
flask.session["credentials"] = credentials_to_dict(credentials)
save_user_credentials(credentials)

Java

まず、User オブジェクトを H2 データベースに保存または更新するメソッドを定義します。

/** Adds or updates a user in the database.
*   @param credential the credentials object to save or update in the database.
*   @param userinfo the userinfo object to save or update in the database.
*   @param session the current session.
*/
public void saveUser(Credential credential, Userinfo userinfo, HttpSession session) {
    User storedUser = null;
    if (session != null && session.getAttribute("login_hint") != null) {
        storedUser = getUser(session.getAttribute("login_hint").toString());
    }

    if (storedUser != null) {
        if (userinfo != null) {
            storedUser.setId(userinfo.getId());
            storedUser.setEmail(userinfo.getEmail());
        }
        userRepository.save(storedUser);
    } else if (credential != null && userinfo != null) {
        User newUser = new User(
            userinfo.getId(),
            userinfo.getEmail(),
        );
        userRepository.save(newUser);
    }
}

認証情報をデータベースに保存するインスタンスは 2 つあります。ユーザーが認可フローの最後でアプリケーションに戻ったときと、API 呼び出しを発行したときです。ここに、以前にセッション credentials キーを設定しました。

/callback ルートの終点で saveUser を呼び出します。user_info オブジェクトは、単にユーザーのメールアドレスを抽出するのではなく、

/** This is the end of the auth flow. We should save user info to the database. */
Userinfo userinfo = authService.getUserInfo(credentials);
saveUser(credentials, userinfo, session);

また、API の呼び出し後に認証情報を更新する必要もあります。この場合、更新された認証情報を saveUser メソッドの引数として指定できます。

/** Save credentials in case access token was refreshed. */
saveUser(credentials, null, session);

期限切れの認証情報

更新トークンが無効になる理由はいくつかあります。以下の内容が含まれます。

  • 更新トークンが 6 か月間使用されていません。
  • ユーザーがアプリのアクセス権限を取り消した。
  • ユーザーがパスワードを変更した。
  • ユーザーが、セッション管理ポリシーが有効になっている Google Cloud 組織に所属している。

認証情報が無効になった場合、ユーザーに承認フローを再度実行して新しいトークンを取得します。

ユーザーを自動的にラウトする

アドオンのランディング ルートを変更して、ユーザーが以前にアプリケーションを承認したかどうかを検出する。該当する場合は、メインのアドオンページに案内します。それ以外の場合は、ログインするように促します。

Python

アプリの起動時にデータベース ファイルが作成されていることを確認します。次のコードをモジュール イニシャライザ(上記の例の webapp/__init__.py など)またはサーバーを起動する main メソッドに挿入します。

# Initialize the database file if not created.
if not os.path.exists(DATABASE_FILE_NAME):
    db.create_all()

その後、上記で説明したように、メソッドで login_hint クエリ パラメータと hd クエリ パラメータを処理する必要があります。次に、リピーターの場合は、ストアの認証情報を読み込みます。login_hint を受信すれば、リピーターであることがわかります。このユーザーに保存されている認証情報を取得し、セッションに読み込みます。

stored_credentials = get_credentials_from_storage(login_hint)

# If we have stored credentials, store them in the session.
if stored_credentials:
    # Load the client secrets file contents.
    client_secrets_dict = json.load(
        open(CLIENT_SECRETS_FILE)).get("web")

    # Update the credentials in the session.
    if not flask.session.get("credentials"):
        flask.session["credentials"] = {}

    flask.session["credentials"] = {
        "token": stored_credentials.access_token,
        "refresh_token": stored_credentials.refresh_token,
        "token_uri": client_secrets_dict["token_uri"],
        "client_id": client_secrets_dict["client_id"],
        "client_secret": client_secrets_dict["client_secret"],
        "scopes": SCOPES
    }

    # Set the username in the session.
    flask.session["username"] = stored_credentials.display_name

最後に、ユーザーの認証情報がない場合はユーザーをログインページに転送します。該当する場合は、メインのアドオンページに案内します。

if "credentials" not in flask.session or \
    flask.session["credentials"]["refresh_token"] is None:
    return flask.render_template("authorization.html")

return flask.render_template(
    "addon-discovery.html",
    message="You've reached the addon discovery page.")

Java

アドオンのランディング ルート(この例では /addon-discovery)に移動します。前述のように、ここで login_hinthd のクエリ パラメータを処理しました。

まず、セッションに認証情報が存在するかどうかを確認します。存在しない場合は、startAuthFlow メソッドを呼び出して、認証フローにユーザーをルーティングします。

/** Check if the credentials exist in the session. The session could have
 *   been cleared when the user clicked the Sign-Out button, and the expected
 *   behavior after sign-out would be to display the sign-in page when the
 *   iframe is opened again. */
if (session.getAttribute("credentials") == null) {
    return startAuthFlow(model);
}

次に、リピーターの場合、H2 データベースからユーザーを読み込みます。login_hint クエリ パラメータを受け取った場合は、リピーターです。ユーザーが H2 データベースに存在する場合、以前に設定した認証情報データストアから認証情報を読み込み、セッションで認証情報を設定します。認証情報が認証情報データストアから取得されなかった場合は、startAuthFlow を呼び出して、認証フローにユーザーをルーティングします。

/** At this point, we know that credentials exist in the session, but we
 *   should update the session credentials with the credentials in persistent
 *   storage in case they were refreshed. If the credentials in persistent
 *   storage are null, we should navigate the user to the authorization flow
 *   to obtain persisted credentials. */

User storedUser = getUser(login_hint);

if (storedUser != null) {
    Credential credential = authService.loadFromCredentialDataStore(login_hint);
    if (credential != null) {
        session.setAttribute("credentials", credential);
    } else {
        return startAuthFlow(model);
    }
}

最後に、アドオンのランディング ページにユーザーを誘導します。

/** Finally, if there are credentials in the session and in persistent
 *   storage, direct the user to the addon-discovery page. */
return "addon-discovery";

アドオンをテストする

教師テストユーザーの 1 人として Google Classroom にログインします。[授業] タブに移動し、新しい課題を作成します。テキスト領域の下にある [アドオン] ボタンをクリックし、アドオンを選択します。iframe が開き、GWM SDK の [アプリの構成] ページで指定したアタッチメントのセットアップ URI がアドオンに読み込まれます。

これで完了です。次のステップ(アタッチメントの作成とユーザーロールの特定)に進む準備が整いました。