これは、Classroom アドオンのチュートリアル シリーズの 3 つ目のチュートリアルです。
このチュートリアルでは、ユーザーに以前に付与された認証情報を自動的に取得して、アドオンへの繰り返しアクセスを処理します。次に、ユーザーを、API リクエストをすぐに発行できるページに転送します。これは Classroom アドオンに必要な動作です。
このチュートリアルでは、次の作業を行います。
- ユーザー認証情報の永続ストレージを実装する。
login_hint
アドオン クエリ パラメータを取得して評価します。これは、ログインしているユーザーの一意の Google ID 番号です。
完了すると、ウェブアプリでユーザーを完全に承認し、Google API を呼び出すことができます。
iframe クエリ パラメータについて
Classroom は、開いたときにアドオンのアタッチメント設定 URI を読み込みます。Classroom は、URI の末尾にいくつかの GET
クエリ パラメータを追加します。これらのパラメータには、有用なコンテキスト情報が含まれています。たとえば、アタッチメント検出 URI が https://example.com/addon
の場合、Classroom はソース URL が https://example.com/addon?courseId=XXX&itemId=YYY&itemType=courseWork&addOnToken=ZZZ
に設定された iframe を作成します。ここで、XXX
、YYY
、ZZZ
は文字列 ID です。このシナリオの詳細については、iframe ガイドをご覧ください。
検出 URL には、次の 5 つのクエリ パラメータを使用できます。
courseId
: 現在の Classroom コースの ID。itemId
: ユーザーが編集または作成するストリーム アイテムの ID。itemType
: ユーザーが作成または編集しているストリーム アイテムの種類(courseWork
、courseWorkMaterial
、announcement
のいずれか)。addOnToken
: 特定の Classroom アドオンの操作を承認するために使用されるトークン。login_hint
: 現在のユーザーの Google ID。
このチュートリアルでは、login_hint
について説明します。ユーザーは、このクエリ パラメータが指定されているかどうかに基づいて、指定されていない場合は認証フロー、指定されている場合はアドオン検出ページに転送されます。
クエリ パラメータにアクセスする
クエリ パラメータは、URI 文字列でウェブ アプリケーションに渡されます。これらの値をセッションに保存します。これらの値は、認可フローや、ユーザーに関する情報の保存と取得に使用されます。これらのクエリ パラメータは、アドオンが最初に開かれたときにのみ渡されます。
Python
Flask ルートの定義に移動します(提供されている例を使用する場合は routes.py
)。アドオン ランディング ルート(この例では /classroom-addon
)の先頭で、login_hint
クエリ パラメータを取得して保存します。
# If the login_hint query parameter is available, we'll store it in the session.
if flask.request.args.get("login_hint"):
flask.session["login_hint"] = flask.request.args.get("login_hint")
login_hint
(存在する場合)がセッションに保存されていることを確認します。これらの値を保存するには、この場所が適しています。これらの値は一時的なもので、アドオンが開かれたときに新しい値が取得されます。
# 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.
login_hint = flask.session.get("login_hint")
# If there's still no login_hint query parameter, this must be their first
# time signing in, so send the user to the sign in page.
if login_hint is None:
return start_auth_flow()
Java
コントローラ クラスのアドオン ランディング ルート(指定された例の AuthController.java
の /addon-discovery
)に移動します。このルートの先頭で、login_hint
クエリ パラメータを取得して保存します。
/** Retrieve the login_hint query parameter from the request URL if present. */
String login_hint = request.getParameter("login_hint");
login_hint
(存在する場合)がセッションに保存されていることを確認します。これらの値を保存するには、この場所が適しています。これらの値は一時的なもので、アドオンが開かれたときに新しい値が取得されます。
/** If login_hint wasn't sent, use the values in the session. */
if (login_hint == null) {
login_hint = (String) session.getAttribute("login_hint");
}
/** If the there is still no login_hint, route the user to the authorization
* page. */
if (login_hint == null) {
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
パラメータは Google の認証サーバーに渡す必要があります。これにより、認証プロセスが容易になります。アプリケーションが認証を試みるユーザーを認識している場合、サーバーはヒントを使用して、ログイン フォームのメール フィールドに事前入力することでログインフローを簡素化します。
Python
Flask サーバー ファイル(この例では /authorize
)の承認ルートに移動します。flow.authorization_url
の呼び出しに login_hint
引数を追加します。
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"),
Java
AuthService.java
クラスの authorize()
メソッドに移動します。login_hint
をパラメータとしてメソッドに追加し、login_hint
と引数を認証 URL ビルダーに追加します。
String authUrl = flow
.newAuthorizationUrl()
.setState(state)
.set("login_hint", login_hint)
.setRedirectUri(REDIRECT_URI)
.build();
ユーザー認証情報の永続ストレージを追加する
アドオンの読み込み時にクエリ パラメータとして login_hint
を受け取った場合は、ユーザーがアプリケーションの認可フローをすでに完了していることを示しています。ユーザーに再ログインを強制するのではなく、以前の認証情報を取得する必要があります。
承認フローの完了時に更新トークンを受け取ったことを思い出してください。このトークンを保存します。このトークンは、アクセス トークンを取得するために再利用されます。アクセス トークンは有効期間が短く、Google API の使用に必要です。以前は、これらの認証情報をセッションに保存していましたが、再訪問を処理するには認証情報を保存する必要があります。
ユーザー スキーマを定義してデータベースを設定する
User
のデータベース スキーマを設定します。
Python
User スキーマを定義する
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 スキーマを定義する
User
には次の属性が含まれます。
id
: ユーザーの Google ID。これは、login_hint
クエリ パラメータで指定された値と一致する必要があります。email
: ユーザーのメールアドレス。
モジュールの resources
ディレクトリに schema.sql
ファイルを作成します。Spring は、このファイルを読み取り、それに応じてデータベースのスキーマを生成します。テーブル名 users
と User
属性 id
と email
を表す列を使用してテーブルを定義します。
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.username
と spring.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.java
の getFlow()
メソッドを更新して、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
まず、H2 データベースに User
オブジェクトを保存または更新するメソッドを定義します。
/** 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
など)またはサーバーを起動するメインメソッドに挿入します。
# Initialize the database file if not created.
if not os.path.exists(DATABASE_FILE_NAME):
db.create_all()
メソッドは、上記で説明したように login_hint
クエリ パラメータを処理する必要があります。次に、リピーターの場合、ストアの認証情報を読み込みます。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_hint
クエリ パラメータを処理しました。
まず、セッションに認証情報が存在するかどうかを確認します。存在しない場合は、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";
アドオンをテストする
教師のテストユーザーとして Google Classroom にログインします。[授業] タブに移動し、新しい [課題] を作成します。テキスト領域の下にある [アドオン] ボタンをクリックし、アドオンを選択します。iframe が開き、Google Workspace Marketplace SDK の [アプリの設定] ページで指定したアタッチメント設定 URI がアドオンによって読み込まれます。
これで、これで、次のステップ(アタッチメントの作成とユーザーのロール識別)に進むことができます。