Обработка повторных входов в систему

Это третье прохождение в серии прохождений над дополнениями Класса.

В этом пошаговом руководстве вы обрабатываете повторные посещения нашего дополнения, автоматически получая ранее предоставленные учетные данные пользователя. Затем вы направляете пользователей на страницы, с которых они могут немедленно отправлять запросы API. Это обязательное поведение для дополнений Класса.

В ходе этого пошагового руководства вы выполните следующее:

  • Внедрите постоянное хранилище для наших учетных данных пользователя.
  • Получите и оцените параметр запроса надстройки login_hint . Это уникальный идентификационный номер Google вошедшего пользователя.

После завершения вы можете полностью авторизовать пользователей в своем веб-приложении и отправлять вызовы API Google.

Понимание параметров запроса iframe

При открытии Classroom загружает URI настройки вложений вашего дополнения. Classroom добавляет к URI несколько параметров запроса GET ; они содержат полезную контекстную информацию. Если, например, ваш URI обнаружения вложений — https://example.com/addon , Classroom создает iframe с исходным URL-адресом, установленным на https://example.com/addon?courseId=XXX&itemId=YYY&itemType=courseWork&addOnToken=ZZZ , где XXX , YYY и ZZZ — идентификаторы строк. Подробное описание этого сценария см. в руководстве по iframes .

Существует пять возможных параметров запроса для URL-адреса обнаружения:

  • courseId : идентификатор текущего курса Classroom.
  • itemId : идентификатор элемента потока, который пользователь редактирует или создает.
  • itemType : тип элемента потока, который пользователь создает или редактирует, например, courseWork , courseWorkMaterial или announcement .
  • addOnToken : токен, используемый для авторизации определенных действий надстройки Класса.
  • login_hint : идентификатор Google текущего пользователя.

В этом пошаговом руководстве рассматривается login_hint . Пользователи направляются в зависимости от того, указан ли этот параметр запроса: либо в поток авторизации, если он отсутствует, либо на страницу обнаружения надстройки, если она присутствует.

Доступ к параметрам запроса

Параметры запроса передаются в ваше веб-приложение в строке URI. Сохраните эти значения в своем сеансе; они используются в процессе авторизации, а также для хранения и получения информации о пользователе. Эти параметры запроса передаются только при первом открытии надстройки.

Питон

Перейдите к определениям ваших маршрутов 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()

Ява

Перейдите к маршруту приземления надстройки в классе вашего контроллера ( /addon-discovery в AuthController.java в приведенном примере). В начале этого маршрута извлеките и сохраните параметр запроса 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. Это облегчает процесс аутентификации; Если ваше приложение знает, какой пользователь пытается пройти аутентификацию, сервер использует подсказку, чтобы упростить процесс входа в систему, предварительно заполнив поле электронной почты в форме входа.

Питон

Перейдите к маршруту авторизации в файле вашего сервера Flask ( /authorize в нашем примере). Добавьте аргумент login_hint к вызову flow.authorization_url .

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"),

Ява

Перейдите к методу authorize() в классе AuthService.java . Добавьте login_hint в качестве параметра метода, а затем добавьте login_hint и аргумент в построитель URL-адресов авторизации.

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

Добавить постоянное хранилище для учетных данных пользователя

Если вы получаете login_hint в качестве параметра запроса при загрузке надстройки, это указывает на то, что пользователь уже завершил процесс авторизации для нашего приложения. Вам следует восстановить их предыдущие учетные данные, а не заставлять их снова входить в систему.

Напомним, что вы получили токен обновления после завершения потока авторизации. Сохраните этот токен; его можно повторно использовать для получения токена доступа , который недолговечен и необходим для использования API Google. Ранее вы сохранили эти учетные данные в сеансе, но вам необходимо сохранить их для обработки повторных посещений.

Определите схему пользователя и настройте базу данных.

Настройте схему базы данных для User .

Питон

Определите схему пользователя

User содержит следующие атрибуты:

  • id : Google ID пользователя. Это должно соответствовать значениям, указанным в параметре запроса login_hint .
  • display_name : имя и фамилия пользователя, например «Алекс Смит».
  • email : адрес электронной почты пользователя.
  • portrait_url : URL-адрес изображения профиля пользователя.
  • refresh_token : ранее полученный токен обновления.

В этом примере хранилище реализуется с использованием SQLite, который изначально поддерживается Python. Он использует модуль 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 на файл data.sqlite в том же каталоге, что и ваш файл main.py

Затем перейдите в каталог вашего модуля и создайте новый файл 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()

Ява

Определите схему пользователя

User содержит следующие атрибуты:

  • id : Google ID пользователя. Оно должно соответствовать значению, указанному в параметре запроса login_hint .
  • email : адрес электронной почты пользователя.

Создайте файл schema.sql в каталоге resources модуля. 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, который можно сохранить в базе данных. Добавьте аннотацию @Table с соответствующим именем таблицы, которое вы настроили schema.sql .

Обратите внимание, что пример кода включает конструкторы и установщики для двух атрибутов. Конструктор и установщики используются в 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; }
}

Создайте интерфейс UserRepository.java для обработки операций CRUD с базой данных. Этот интерфейс расширяет интерфейс 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;
}

Настройте базу данных

Для хранения информации, связанной с пользователем, используйте базу данных H2, которая изначально поддерживается Spring Boot. Эта база данных также используется в последующих пошаговых руководствах для хранения другой информации, связанной с Классом. Для настройки базы данных 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 , обновите конфигурацию и перезапустите приложение.

Настройка update конфигурации spring.jpa.hibernate.ddl-auto гарантирует сохранение данных, хранящихся в базе данных, при перезапуске приложения. Чтобы очищать базу данных при каждом перезапуске приложения, установите для этой конфигурации значение create .

Установите для конфигурации spring.jpa.open-in-view значение false . Эта конфигурация включена по умолчанию, и известно, что она приводит к проблемам с производительностью, которые трудно диагностировать в рабочей среде.

Как описано ранее, у вас должна быть возможность получить учетные данные постоянного пользователя. Этому способствует встроенная поддержка хранилища учетных данных, предлагаемая GoogleAuthorizationCodeFlow .

В классе AuthService.java определите путь к файлу, в котором хранится класс учетных данных. В этом примере файл создается в каталоге /credentialStore . Добавьте путь к хранилищу учетных данных в .gitignore . Этот каталог создается после того, как пользователь начинает процесс авторизации.

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

Затем создайте в файле AuthService.java метод, который создает и возвращает объект FileDataStoreFactory . Это хранилище данных, в котором хранятся учетные данные.

/** 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;
}

Обновите метод getFlow() в AuthService.java , включив setDataStoreFactory в метод GoogleAuthorizationCodeFlow Builder() , и вызовите getCredentialDataStore() чтобы установить хранилище данных.

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

Затем обновите метод getAndSaveCredentials(String authorizationCode) . Раньше этот метод получал учетные данные, не сохраняя их нигде. Обновите метод для хранения учетных данных в хранилище данных, индексированном по идентификатору пользователя.

Идентификатор пользователя можно получить из объекта TokenResponse с помощью id_token , но сначала его необходимо проверить. В противном случае клиентские приложения могут выдавать себя за пользователей, отправляя на сервер измененные идентификаторы пользователей. рекомендуется использовать клиентские библиотеки Google API для проверки id_token . Дополнительную информацию см. на [странице Google Identity, посвященной проверке токена Google ID].

// 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 , который вы можете использовать для получения конкретной записи пользователя.

Питон

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

Ява

В классе AuthController.java определите метод для извлечения пользователя из базы данных на основе его идентификатора пользователя.

/** 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;
}

Учетные данные магазина

Существует два сценария хранения учетных данных. Если id пользователя уже есть в базе данных, обновите существующую запись, добавив новые значения. В противном случае создайте новую запись User и добавьте ее в базу данных.

Питон

Сначала определите служебный метод, который реализует поведение хранения или обновления.

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()

Есть два случая, когда вы можете сохранить учетные данные в своей базе данных: когда пользователь возвращается в ваше приложение в конце потока авторизации и при выполнении вызова API. Здесь мы ранее установили ключ credentials сеанса.

Вызовите save_user_credentials в конце маршрута callback . Сохраните объект 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)

Ява

Сначала определите метод, который сохраняет или обновляет объект 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);
    }
}

Есть два случая, когда вы можете сохранить учетные данные в своей базе данных: когда пользователь возвращается в ваше приложение в конце потока авторизации и при выполнении вызова API. Здесь мы ранее установили ключ credentials сеанса.

Вызовите saveUser в конце маршрута /callback . Вам следует сохранить объект 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);

Срок действия учетных данных истек

Обратите внимание, что существует несколько причин, по которым токены обновления могут стать недействительными. К ним относятся:

  • Токен обновления не использовался в течение шести месяцев.
  • Пользователь отзывает разрешения на доступ вашего приложения.
  • Пользователь меняет пароли.
  • Пользователь принадлежит к организации Google Cloud, в которой действуют политики управления сеансами.

Получите новые токены, повторно отправив пользователя через поток авторизации, если его учетные данные станут недействительными.

Автоматически маршрутизировать пользователя

Измените дополнительный маршрут перехода, чтобы определить, авторизовал ли пользователь наше приложение ранее. Если да, направьте их на нашу главную страницу дополнения. В противном случае предложите им войти в систему.

Питон

Убедитесь, что файл базы данных был создан при запуске приложения. Вставьте следующее в инициализатор модуля (например, 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.")

Ява

Перейдите к маршруту приземления вашего дополнения ( /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, и надстройка загрузит URI настройки вложения , который вы указали на странице конфигурации приложения SDK Google Workspace Marketplace.

Поздравляем! Вы готовы перейти к следующему шагу: созданию вложений и определению роли пользователя .