Processar logins repetidos

Este é o terceiro tutorial da série de complementos do Google Sala de Aula.

Neste tutorial, você processa visitas repetidas ao nosso complemento recuperando automaticamente as credenciais concedidas anteriormente a um usuário. Em seguida, você encaminha os usuários para páginas em que eles podem emitir solicitações de API imediatamente. Esse é um comportamento necessário para os complementos do Google Sala de Aula.

Durante este tutorial, você vai:

  • Implementar armazenamento permanente para nossas credenciais de usuário.
  • Extraia e avalie o parâmetro de consulta do complemento login_hint. É um número de ID do Google exclusivo do usuário conectado.

Depois disso, você pode autorizar totalmente os usuários no seu app da Web e emitir chamadas para as APIs do Google.

Entender os parâmetros de consulta do iframe

O Google Sala de Aula carrega o URI de configuração de anexo do complemento ao ser aberto. O Google Sala de Aula adiciona vários parâmetros de consulta GET ao URI, que contêm informações contextuais úteis. Se, por exemplo, o URI de descoberta de anexos for https://example.com/addon, o Google Sala de Aula vai criar o iframe com o URL de origem definido como https://example.com/addon?courseId=XXX&itemId=YYY&itemType=courseWork&addOnToken=ZZZ, em que XXX, YYY e ZZZ são IDs de string. Consulte o guia de iframes para uma descrição detalhada desse cenário.

Há cinco parâmetros de consulta possíveis para o URL de descoberta:

  • courseId: o ID do curso atual do Google Sala de Aula.
  • itemId: o ID do item de transmissão que o usuário está editando ou criando.
  • itemType: o tipo de item de transmissão que o usuário está criando ou editando, um dos courseWork, courseWorkMaterial ou announcement.
  • addOnToken: um token usado para autorizar determinadas ações de complementos do Google Sala de Aula.
  • login_hint: o ID do Google do usuário atual.

Este tutorial aborda login_hint. Os usuários são direcionados com base na disponibilidade desse parâmetro de consulta, seja para o fluxo de autorização, se estiver ausente, ou para a página de descoberta de complementos, se estiver presente.

Acessar os parâmetros de consulta

Os parâmetros de consulta são transmitidos para o aplicativo da Web na string de URI. Armazene esses valores na sua sessão. Eles são usados no fluxo de autorização e para armazenar e recuperar informações sobre o usuário. Esses parâmetros de consulta são transmitidos apenas quando o complemento é aberto pela primeira vez.

Python

Navegue até as definições das rotas do Flask (routes.py se você estiver seguindo nosso exemplo). Na parte de cima do caminho de destino do complemento (/classroom-addon no exemplo fornecido), recupere e armazene o parâmetro de consulta 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")

Verifique se login_hint (se presente) está armazenado na sessão. Esse é um local adequado para armazenar esses valores. Eles são temporários, e você recebe novos valores quando o complemento é aberto.

# 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

Navegue até a rota de destino do complemento na classe do controlador (/addon-discovery em AuthController.java no exemplo fornecido). No início desta rota, extraia e armazene o parâmetro de consulta login_hint.

/** Retrieve the login_hint query parameter from the request URL if present. */
String login_hint = request.getParameter("login_hint");

Verifique se login_hint (se presente) está armazenado na sessão. Esse é um local adequado para armazenar esses valores. Eles são temporários, e você recebe novos valores quando o complemento é aberto.

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

Adicionar os parâmetros de consulta ao fluxo de autorização

O parâmetro login_hint também precisa ser transmitido aos servidores de autenticação do Google. Isso facilita o processo de autenticação. Se o aplicativo souber qual usuário está tentando fazer a autenticação, o servidor vai usar a dica para simplificar o fluxo de login preenchendo o campo de e-mail no formulário de login.

Python

Navegue até a rota de autorização no arquivo do servidor Flask (/authorize no exemplo fornecido). Adicione o argumento login_hint à chamada para 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"),

Java

Navegue até o método authorize() na classe AuthService.java. Adicione login_hint como um parâmetro ao método e adicione login_hint e o argumento ao criador de URL de autorização.

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

Adicionar armazenamento permanente para credenciais do usuário

Se você receber login_hint como um parâmetro de consulta quando o complemento for carregado, isso indica que o usuário já concluiu o fluxo de autorização para nosso aplicativo. Você precisa recuperar as credenciais anteriores em vez de forçar os usuários a fazer login novamente.

Você recebeu um token de atualização após a conclusão do fluxo de autorização. Salve esse token, que será reutilizado para receber um token de acesso, que é de curta duração e necessário para usar as APIs do Google. Você salvou essas credenciais na sessão, mas precisa armazená-las para processar visitas repetidas.

Definir o esquema de usuário e configurar o banco de dados

Configurar um esquema de banco de dados para um User.

Python

Definir o esquema de usuário

Um User contém os seguintes atributos:

  • id: o ID do usuário do Google. Ele precisa corresponder aos valores fornecidos no parâmetro de consulta login_hint.
  • display_name: o nome e o sobrenome do usuário, como "Alex Smith".
  • email: o endereço de e-mail do usuário.
  • portrait_url: o URL da foto do perfil do usuário.
  • refresh_token: o token de atualização adquirido anteriormente.

Este exemplo implementa o armazenamento usando o SQLite, que tem suporte nativo do Python. Ele usa o módulo flask_sqlalchemy para facilitar o gerenciamento do nosso banco de dados.

Configurar o banco de dados

Primeiro, especifique um local de arquivo para nosso banco de dados. Navegue até o arquivo de configuração do servidor (config.py no exemplo fornecido) e adicione o seguinte.

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

Isso aponta o Flask para o arquivo data.sqlite no mesmo diretório que o main.py.

Em seguida, navegue até o diretório do módulo e crie um novo arquivo models.py. Isso é webapp/models.py se você estiver seguindo nosso exemplo. Adicione o seguinte ao novo arquivo para definir a tabela User, substituindo o nome do módulo por webapp, se for diferente.

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

Por fim, no arquivo __init__.py do módulo, adicione o seguinte para importar os novos modelos e criar o banco de dados.

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

Definir o esquema de usuário

Um User contém os seguintes atributos:

  • id: o ID do usuário do Google. Ele precisa corresponder ao valor fornecido no parâmetro de consulta login_hint.
  • email: o endereço de e-mail do usuário.

Crie um arquivo schema.sql no diretório resources do módulo. O Spring lê esse arquivo e gera um esquema para o banco de dados. Defina a tabela com um nome, users, e colunas para representar os atributos User, id e email.

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

Crie uma classe Java para definir o modelo User do banco de dados. Isso é User.java no exemplo fornecido.

Adicione a anotação @Entity para indicar que este é um POJO que pode ser salvo no banco de dados. Adicione a anotação @Table com o nome da tabela correspondente que você configurou em schema.sql.

O exemplo de código inclui construtores e setters para os dois atributos. O construtor e os setters são usados em AuthController.java para criar ou atualizar um usuário no banco de dados. Você também pode incluir getters e um método toString como achar melhor, mas, para este tutorial específico, esses métodos não são usados e são omitidos do exemplo de código nesta página para encurtar.

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

Crie uma interface chamada UserRepository.java para processar operações CRUD no banco de dados. Essa interface estende a interface CrudRepository.

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

A classe controller facilita a comunicação entre o cliente e o repositório. Portanto, atualize o construtor da classe do controlador para injetar a classe 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;
}

Configurar o banco de dados

Para armazenar informações relacionadas ao usuário, use um banco de dados H2 com suporte inerente ao Spring Boot. Esse banco de dados também é usado em tutoriais subsequentes para armazenar outras informações relacionadas ao Google Sala de Aula. A configuração do banco de dados H2 requer a adição da seguinte configuração a 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

A configuração spring.datasource.url cria um diretório, chamado h2, com o arquivo userdb armazenado nele. Adicione o caminho para o banco de dados H2 ao .gitignore. É necessário atualizar o spring.datasource.username e spring.datasource.password antes de executar o aplicativo para definir o banco de dados com um nome de usuário e uma senha de sua escolha. Para atualizar o nome de usuário e a senha do banco de dados após a execução do aplicativo, exclua o diretório h2 gerado, atualize a configuração e execute o aplicativo novamente.

Definir a configuração spring.jpa.hibernate.ddl-auto como update garante que os dados armazenados no banco de dados sejam preservados quando o aplicativo for reiniciado. Para limpar o banco de dados sempre que o aplicativo for reiniciado, defina essa configuração como create.

Defina a configuração spring.jpa.open-in-view como false. Essa configuração é ativada por padrão e pode resultar em problemas de desempenho que são difíceis de diagnosticar na produção.

Como descrito anteriormente, você precisa extrair as credenciais de um usuário recorrente. Isso é facilitado pelo suporte à loja de credenciais integrada oferecida pelo GoogleAuthorizationCodeFlow.

Na classe AuthService.java, defina um caminho para o arquivo em que a classe de credencial está armazenada. Neste exemplo, o arquivo é criado no diretório /credentialStore. Adicione o caminho para a loja de credenciais ao .gitignore. Esse diretório é gerado quando o usuário inicia o fluxo de autorização.

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

Em seguida, crie um método no arquivo AuthService.java que crie e retorne um objeto FileDataStoreFactory. Esse é o repositório de dados que armazena credenciais.

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

Atualize o método getFlow() em AuthService.java para incluir setDataStoreFactory no método GoogleAuthorizationCodeFlow Builder() e chame getCredentialDataStore() para definir o repositório de dados.

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

Em seguida, atualize o método getAndSaveCredentials(String authorizationCode). Antes, esse método conseguia credenciais sem armazená-las em nenhum lugar. Atualize o método para armazenar as credenciais no repositório de dados indexado pelo ID do usuário.

O ID do usuário pode ser obtido do objeto TokenResponse usando o id_token, mas ele precisa ser verificado primeiro. Caso contrário, os aplicativos de cliente poderão falsificar a identidade de usuários enviando IDs de usuário modificados para o servidor. É recomendável usar as bibliotecas de cliente das APIs do Google para validar o id_token. Consulte a [página de identidade do Google sobre como verificar o token de ID do Google] para mais informações.

// 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.");
}

Depois que o id_token for verificado, obtenha o userId para armazenar junto com as credenciais recebidas.

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

Atualize a chamada para flow.createAndStoreCredential para incluir userId.

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

Adicione um método à classe AuthService.java que retorne as credenciais de um usuário específico, se ele existir no repositório de dados.

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

Recuperar credenciais

Defina um método para buscar Users. Você recebe um id no parâmetro de consulta login_hint, que pode ser usado para recuperar um registro de usuário específico.

Python

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

Java

Na classe AuthController.java, defina um método para recuperar um usuário do banco de dados com base no ID do usuário.

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

Armazenar credenciais

Há dois cenários para armazenar credenciais. Se o id do usuário já estiver no banco de dados, atualize o registro com os novos valores. Caso contrário, crie um novo registro User e adicione-o ao banco de dados.

Python

Primeiro, defina um método utilitário que implemente o comportamento de armazenamento ou de atualização.

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

Há duas instâncias em que você pode salvar credenciais no banco de dados: quando o usuário retorna ao seu aplicativo no final do fluxo de autorização e ao emitir uma chamada de API. É aqui que definimos anteriormente a chave credentials da sessão.

Chame save_user_credentials no final da rota callback. Mantenha o objeto user_info em vez de extrair apenas o nome do usuário.

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

Também é necessário atualizar as credenciais após as chamadas para a API. Nesse caso, é possível fornecer as credenciais atualizadas como argumentos para o método save_user_credentials.

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

Java

Primeiro, defina um método que armazene ou atualize um objeto User no banco de dados 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);
    }
}

Há duas instâncias em que você pode salvar credenciais no banco de dados: quando o usuário retorna ao seu aplicativo no final do fluxo de autorização e ao emitir uma chamada de API. É aqui que definimos anteriormente a chave credentials da sessão.

Chame saveUser no final da rota /callback. Mantenha o objeto user_info em vez de apenas extrair o e-mail do usuário.

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

Também é necessário atualizar as credenciais após as chamadas para a API. Nesse caso, é possível fornecer as credenciais atualizadas como argumentos para o método saveUser.

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

Credenciais expiradas

Há alguns motivos para que os tokens de atualização possam ficar inválidos. Veja alguns exemplos:

  • O token de atualização não foi usado há seis meses.
  • O usuário revoga as permissões de acesso do app.
  • O usuário muda as senhas.
  • O usuário pertence a uma organização do Google Cloud que tem políticas de controle de sessão em vigor.

Adquira novos tokens enviando o usuário novamente pelo fluxo de autorização se as credenciais dele se tornarem inválidas.

Encaminhar o usuário automaticamente

Modifique a rota de destino do complemento para detectar se o usuário já autorizou nosso aplicativo. Se sim, encaminhe para nossa página principal de complementos. Caso contrário, peça para que façam login.

Python

Verifique se o arquivo do banco de dados foi criado quando o aplicativo foi iniciado. Insira o seguinte em um inicializador de módulo (como webapp/__init__.py no exemplo fornecido) ou no método principal que inicializa o servidor.

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

Seu método precisa processar o parâmetro de consulta login_hint conforme discutido acima. Em seguida, carregue as credenciais da loja se for um visitante recorrente. Você sabe que é um visitante recorrente se recebeu login_hint. Extraia as credenciais armazenadas para esse usuário e carregue-as na sessão.

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

Por fim, redirecione o usuário para a página de login se não tivermos as credenciais dele. Se sim, redirecione para a página principal do complemento.

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

Navegue até a rota de destino do complemento (/addon-discovery no exemplo fornecido). Como discutido acima, foi aqui que você processou o parâmetro de consulta login_hint.

Primeiro, verifique se há credenciais na sessão. Caso contrário, encaminhe o usuário pelo fluxo de autenticação chamando o método 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);
}

Em seguida, carregue o usuário do banco de dados H2 se ele for um visitante recorrente. É um visitante recorrente se você receber o parâmetro de consulta login_hint. Se o usuário existir no banco de dados H2, carregue as credenciais do armazenamento de credenciais configurado anteriormente e defina as credenciais na sessão. Se as credenciais não tiverem sido obtidas do repositório de credenciais, encaminhe o usuário pelo fluxo de autenticação chamando 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);
    }
}

Por fim, direcione o usuário para a página de destino do complemento.

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

Testar o complemento

Faça login no Google Sala de Aula como um dos seus usuários de teste Professor. Acesse a guia Atividades e crie uma nova Atividade. Clique no botão Complementos abaixo da área de texto e selecione o complemento. O iframe é aberto, e o complemento carrega o URI de configuração de anexo que você especificou na página Configuração do app do SDK do Google Workspace Marketplace.

Parabéns! Você já pode prosseguir para a próxima etapa: criar anexos e identificar a função do usuário.