Processar logins repetidos

Este é o terceiro tutorial da série de complementos da 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 de 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 fornecido). 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. Ele 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 banco de dados.

Configurar o banco de dados

Primeiro, especifique um local de arquivo para o 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 código abaixo 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. Esse valor 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 conforme 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 o 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 recuperar 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 armazenamento 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 modificados para o servidor. É recomendável usar as bibliotecas de cliente da API 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 extrair 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 ao armazenar credenciais. Se o id do usuário já estiver no banco de dados, atualize o registro com novos valores. Caso contrário, crie um novo registro User e adicione 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.